This commit is contained in:
seb
2026-09-29 10:27:23 +02:00
parent 62d46a1df3
commit 3089cc7252
8 changed files with 454 additions and 34 deletions

View File

@@ -1,5 +1,10 @@
# Server
PORT=4208
# HTTPS server port (defaults to 8443, set to 0 or false to disable)
HTTPS_PORT=8443
# Optional custom TLS cert/key paths (defaults to auto-generated certs/key.pem and certs/cert.pem)
# HTTPS_KEY_PATH=./certs/key.pem
# HTTPS_CERT_PATH=./certs/cert.pem
BODY_LIMIT=100mb
# CORS — comma-separated allowed origins

3
.gitignore vendored
View File

@@ -2,4 +2,5 @@ node_modules/
.env
data/
logs/
print_spool/
print_spool/
certs/

View File

@@ -2,6 +2,7 @@
```
http://localhost:4208/api
https://localhost:8443/api
```
## CORS & cookies

301
package-lock.json generated
View File

@@ -26,6 +26,7 @@
"multer": "^2.4.0",
"react": "^19.3.0",
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0",
"sharp": "^0.35.4"
},
"devDependencies": {
@@ -708,6 +709,205 @@
"node": ">=18"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@peculiar/asn1-cms": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.10.0.tgz",
"integrity": "sha512-CkX0H4NCIOMHOU3rh2xXZywinYZ/EnJlaOlJiGI0e5L4XjFqHf4iH30LMbXWChVppdA9ljbanjtzQFOhDNfTWg==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"@peculiar/asn1-x509-attr": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-csr": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.10.0.tgz",
"integrity": "sha512-jTPTr/9rxKM+niQLMF3jiAMb0lWHUhUHIuSOhdGCIfpB4FAQYf9SoiXOgxP8bS/68IB+bj+1OHvVnDVCQfVUZw==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-ecc": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.10.0.tgz",
"integrity": "sha512-GFd3iOjFrWX+QWH2R2dO5QSJyyRGv9CIBKtRlGlPNCvb2RvmCbBDexe91MJgV76c69d998Xsa+CvuQ98seoiPg==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-pfx": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.10.0.tgz",
"integrity": "sha512-1y3QK9ZH1IPleAMmRoJlPS10eGkAWnULETW8zDFNUK1ffqpG7zmDY+kYBMYQgwlcQZ2iitLm2z2EBP0xzRXpaA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.10.0",
"@peculiar/asn1-pkcs8": "^2.10.0",
"@peculiar/asn1-rsa": "^2.10.0",
"@peculiar/asn1-schema": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-pkcs8": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.10.0.tgz",
"integrity": "sha512-Ri+BZT9bnwqlHWmhs7lvWjvJRxKev2hA2+4EbZajgeYWCbR8hyv0znF4DhsFouOhMj2nSDV/iGJ6DMQtwITnCw==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-pkcs9": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.10.0.tgz",
"integrity": "sha512-XIXsbDQFezYk6fudczkuvawkRD4GNpISGCqfYhdkJlvcGF/RFknU+0DDJagOaJqBf+PdPzk4J9oMqDGcvfR9HQ==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.10.0",
"@peculiar/asn1-pfx": "^2.10.0",
"@peculiar/asn1-pkcs8": "^2.10.0",
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"@peculiar/asn1-x509-attr": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-rsa": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.10.0.tgz",
"integrity": "sha512-4Jvmwlh3gZAhNZ4/u7JSyLuce9hofOFZ4o3PYKAccCImGnyaT5CMym/ATgvAvqpOYFNW531oPZvJlEBhJy9VfA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.10.0.tgz",
"integrity": "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==",
"license": "MIT",
"dependencies": {
"@peculiar/utils": "^2.0.2",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-x509": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.10.0.tgz",
"integrity": "sha512-ucNVg8+ANveTpMN3fy9lA2alryONdXc2A4cEG2hMniWbvQt+YOZoe8BI80YYNO8FBcuDY1qbDQ4uQGQVraHxGA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/utils": "^2.0.2",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/asn1-x509-attr": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.10.0.tgz",
"integrity": "sha512-/85GtKOKmgvuSJNlaFfwGWNdRSZZ+hpF02NyM01XiCdzpaZONiBltDyfluFPvFx966CR+ZHNSG1jniwpy07oGg==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.10.0",
"@peculiar/asn1-x509": "^2.10.0",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/@peculiar/x509": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
"integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-cms": "^2.6.0",
"@peculiar/asn1-csr": "^2.6.0",
"@peculiar/asn1-ecc": "^2.6.0",
"@peculiar/asn1-pkcs9": "^2.6.0",
"@peculiar/asn1-rsa": "^2.6.0",
"@peculiar/asn1-schema": "^2.6.0",
"@peculiar/asn1-x509": "^2.6.0",
"pvtsutils": "^1.3.6",
"reflect-metadata": "^0.2.2",
"tslib": "^2.8.1",
"tsyringe": "^4.10.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -821,6 +1021,20 @@
"dev": true,
"license": "Python-2.0"
},
"node_modules/asn1js": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
"license": "BSD-3-Clause",
"dependencies": {
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.5",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/auto-bind": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz",
@@ -1009,6 +1223,15 @@
"node": ">= 0.8"
}
},
"node_modules/bytestreamjs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
"integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -2918,6 +3141,26 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/pkijs": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz",
"integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==",
"license": "BSD-3-Clause",
"workspaces": [
"website"
],
"dependencies": {
"@noble/hashes": "1.8.0",
"asn1js": "^3.0.6",
"bytestreamjs": "^2.0.1",
"pvtsutils": "^1.3.6",
"pvutils": "^1.1.3",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -2987,6 +3230,24 @@
"once": "^1.3.1"
}
},
"node_modules/pvtsutils": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/pvutils": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz",
"integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
@@ -3142,6 +3403,12 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"license": "Apache-2.0"
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3231,6 +3498,19 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/selfsigned": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz",
"integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==",
"license": "MIT",
"dependencies": {
"@peculiar/x509": "^1.14.2",
"pkijs": "^3.3.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
@@ -3709,8 +3989,25 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"optional": true
"license": "0BSD"
},
"node_modules/tsyringe": {
"version": "4.10.0",
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
"license": "MIT",
"dependencies": {
"tslib": "^1.9.3"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/tsyringe/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",

View File

@@ -31,6 +31,7 @@
"multer": "^2.4.0",
"react": "^19.3.0",
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0",
"sharp": "^0.35.4"
},
"devDependencies": {

View File

@@ -1,3 +1,5 @@
import http from 'http';
import https from 'https';
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
@@ -9,6 +11,7 @@ import env from '../config/env.js';
import { authMiddleware } from './middleware/auth.js';
import { ensureConnection } from '../db/store.js';
import { closeAll as closeAllSse } from './sse.js';
import { getOrCreateCert } from '../util/cert.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -36,6 +39,7 @@ async function loadRoutes(app, basePath = '') {
}
let server;
let httpsServer;
let starting;
let startedAt;
@@ -95,8 +99,34 @@ async function createApi() {
return app;
}
function listenServer(instance, port, protocol) {
return new Promise((resolve, reject) => {
instance.once('listening', () => {
resolve();
});
instance.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
let detail = '';
try {
const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf8' }).trim();
if (pids) detail = ` (PID ${pids.replace(/\n/g, ', ')})`;
} catch {
// lsof unavailable or no match
}
console.error(`Port ${port} (${protocol}) is already in use${detail}. Check with: lsof -i :${port}`);
} else {
console.error(`${protocol} server error:`, err.message);
}
reject(err);
});
instance.listen(port);
});
}
export async function startApi() {
if (server) {
if (server || httpsServer) {
return server;
}
if (starting) {
@@ -109,31 +139,22 @@ export async function startApi() {
startedAt = new Date().toISOString();
const app = await createApi();
await new Promise((resolve, reject) => {
const instance = app.listen(env.port);
const httpInstance = http.createServer(app);
await listenServer(httpInstance, env.port, 'HTTP');
server = httpInstance;
console.log(`API listening on port ${env.port} (HTTP)`);
instance.once('listening', () => {
server = instance;
console.log(`API listening on port ${env.port}`);
resolve();
});
instance.once('error', (err) => {
if (err.code === 'EADDRINUSE') {
let detail = '';
try {
const pids = execSync(`lsof -ti :${env.port}`, { encoding: 'utf8' }).trim();
if (pids) detail = ` (PID ${pids.replace(/\n/g, ', ')})`;
} catch {
// lsof unavailable or no match
}
console.error(`Port ${env.port} is already in use${detail}. Check with: lsof -i :${env.port}`);
} else {
console.error('Server error:', err.message);
}
reject(err);
});
});
if (env.httpsPort) {
try {
const tls = await getOrCreateCert();
const httpsInstance = https.createServer(tls, app);
await listenServer(httpsInstance, env.httpsPort, 'HTTPS');
httpsServer = httpsInstance;
console.log(`API listening on port ${env.httpsPort} (HTTPS self-signed)`);
} catch (err) {
console.error('Failed to start HTTPS server:', err.message);
}
}
return server;
})();
@@ -146,13 +167,26 @@ export async function startApi() {
}
export async function stopApi() {
if (!server) return;
if (!server && !httpsServer) return;
// Long-lived SSE (and keep-alive) sockets otherwise block server.close()
// forever — node --watch hangs on "Waiting for graceful termination...".
closeAllSse();
await new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
server.closeAllConnections();
});
server = undefined;
const closers = [];
if (server) {
const s = server;
server = undefined;
closers.push(new Promise((resolve, reject) => {
s.close((err) => (err ? reject(err) : resolve()));
s.closeAllConnections();
}));
}
if (httpsServer) {
const s = httpsServer;
httpsServer = undefined;
closers.push(new Promise((resolve, reject) => {
s.close((err) => (err ? reject(err) : resolve()));
s.closeAllConnections();
}));
}
await Promise.all(closers);
}

View File

@@ -5,8 +5,18 @@ import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: resolve(__dirname, '../../.env'), quiet: true });
function parseHttpsPort(val) {
if (val === undefined || val === '') return 8443;
if (val === 'false' || val === '0' || val === 'none' || val === 'disabled') return null;
const n = Number(val);
return Number.isInteger(n) && n > 0 ? n : 8443;
}
export default {
port: Number(process.env.PORT) || 4208,
httpsPort: parseHttpsPort(process.env.HTTPS_PORT),
httpsKeyPath: process.env.HTTPS_KEY_PATH || resolve(__dirname, '../../certs/key.pem'),
httpsCertPath: process.env.HTTPS_CERT_PATH || resolve(__dirname, '../../certs/cert.pem'),
bodyLimit: process.env.BODY_LIMIT || '100mb',
corsOrigins: (process.env.CORS_ORIGINS || 'http://localhost:5173')
.split(',')

71
src/util/cert.js Normal file
View File

@@ -0,0 +1,71 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import selfsigned from 'selfsigned';
import env from '../config/env.js';
function getLocalIPv4() {
const addresses = [];
for (const iface of Object.values(os.networkInterfaces())) {
if (!iface) continue;
for (const addr of iface) {
if (addr.family === 'IPv4' && !addr.internal) {
addresses.push(addr.address);
}
}
}
return addresses;
}
/**
* Loads existing TLS credentials or generates a self-signed certificate.
* @returns {Promise<{ key: Buffer | string, cert: Buffer | string }>}
*/
export async function getOrCreateCert() {
const keyPath = env.httpsKeyPath;
const certPath = env.httpsCertPath;
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
return {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
};
}
const keyDir = path.dirname(keyPath);
if (!fs.existsSync(keyDir)) {
fs.mkdirSync(keyDir, { recursive: true });
}
const certDir = path.dirname(certPath);
if (!fs.existsSync(certDir)) {
fs.mkdirSync(certDir, { recursive: true });
}
const altNames = [
{ type: 2, value: 'localhost' },
{ type: 7, ip: '127.0.0.1' },
];
for (const ip of getLocalIPv4()) {
altNames.push({ type: 7, ip });
}
const pems = await selfsigned.generate(
[{ name: 'commonName', value: 'localhost' }],
{
days: 365,
algorithm: 'sha256',
extensions: [{ name: 'subjectAltName', altNames }],
}
);
fs.writeFileSync(keyPath, pems.private, { mode: 0o600 });
fs.writeFileSync(certPath, pems.cert);
console.log(`[tls] Generated self-signed certificate at ${certPath}`);
return {
key: pems.private,
cert: pems.cert,
};
}