feat: add Timestamp PDF tool with RFC 3161 support

Add document timestamping to the Secure PDF section using RFC 3161
protocol. Users can select from preset TSA servers (DigiCert, Sectigo,
SSL.com, Entrust, FreeTSA) or enter a custom TSA URL. No personal
certificate is required — only a cryptographic hash is sent to the server.

Key changes:
- Timestamp PDF page with TSA server selector, FAQ and SEO structured data
- timestampPdf() function with CORS proxy URL resolution
- TimestampNode for the workflow engine
- Tool entry in Secure PDF category + homepage i18n
- Built-in CORS proxy middleware for dev/preview
- Translations for all 16 languages

Tested with DigiCert, Sectigo and Entrust TSA servers. Timestamps are
verifiable in Adobe Acrobat (ETSI.RFC3161 SubFilter).
This commit is contained in:
InstalZDLL
2026-03-15 00:30:53 +01:00
parent 2de36b6605
commit dfd0ebcfc5
26 changed files with 1127 additions and 11 deletions

View File

@@ -1,5 +1,7 @@
import { defineConfig, Plugin } from 'vitest/config';
import type { IncomingMessage, ServerResponse } from 'http';
import http from 'http';
import https from 'https';
import type { Connect } from 'vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
import tailwindcss from '@tailwindcss/vite';
@@ -197,13 +199,89 @@ function createLanguageMiddleware(isDev: boolean): Connect.NextHandleFunction {
};
}
function createCorsProxyMiddleware(): Connect.NextHandleFunction {
return (
req: IncomingMessage,
res: ServerResponse,
next: Connect.NextFunction
): void => {
if (!req.url?.startsWith('/cors-proxy')) return next();
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.statusCode = 204;
res.end();
return;
}
const parsed = new URL(req.url, 'http://localhost');
const targetUrl = parsed.searchParams.get('url');
if (!targetUrl) {
res.statusCode = 400;
res.end('Missing url parameter');
return;
}
console.log(`[CORS Proxy] ${req.method} ${targetUrl}`);
const bodyChunks: Buffer[] = [];
req.on('data', (chunk: Buffer) => bodyChunks.push(chunk));
req.on('end', () => {
const body = Buffer.concat(bodyChunks);
const target = new URL(targetUrl);
const transport = target.protocol === 'https:' ? https : http;
const headers: Record<string, string> = {};
if (req.headers['content-type']) {
headers['Content-Type'] = req.headers['content-type'] as string;
}
if (body.length > 0) {
headers['Content-Length'] = String(body.length);
}
const proxyReq = transport.request(
targetUrl,
{ method: req.method || 'GET', headers },
(proxyRes) => {
console.log(
`[CORS Proxy] Response: ${proxyRes.statusCode} from ${targetUrl}`
);
res.setHeader(
'Access-Control-Allow-Origin',
req.headers.origin || '*'
);
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.statusCode = proxyRes.statusCode || 200;
proxyRes.pipe(res);
}
);
proxyReq.on('error', (err) => {
console.error('[CORS Proxy] Error:', err.message);
res.statusCode = 502;
res.end(`Proxy error: ${err.message}`);
});
if (body.length > 0) {
proxyReq.write(body);
}
proxyReq.end();
});
};
}
function languageRouterPlugin(): Plugin {
return {
name: 'language-router',
configureServer(server) {
server.middlewares.use(createCorsProxyMiddleware());
server.middlewares.use(createLanguageMiddleware(true));
},
configurePreviewServer(server) {
server.middlewares.use(createCorsProxyMiddleware());
server.middlewares.use(createLanguageMiddleware(false));
},
};
@@ -309,7 +387,7 @@ export default defineConfig(() => {
include: ['buffer', 'stream', 'util', 'zlib', 'process'],
globals: {
Buffer: true,
global: true,
global: false,
process: true,
},
}),
@@ -551,6 +629,7 @@ export default defineConfig(() => {
__dirname,
'src/pages/digital-sign-pdf.html'
),
'timestamp-pdf': resolve(__dirname, 'src/pages/timestamp-pdf.html'),
'validate-signature-pdf': resolve(
__dirname,
'src/pages/validate-signature-pdf.html'