fix: PDF Timestamp bug

This commit is contained in:
alam00000 committed 2026-05-01 13:02:53 +05:30
1 parent 6f5c75b913
commit ea53689bba
27 files changed
+201 -59

No files matched your search

+31 -1
View File
@@ -153,8 +153,21 @@ async function generateProxySignature(
.join('');
}
function resolveProxyBase(): string {
if (
CORS_PROXY_URL.startsWith('http://') ||
CORS_PROXY_URL.startsWith('https://')
) {
return CORS_PROXY_URL;
}
if (typeof window !== 'undefined' && window.location?.origin) {
return new URL(CORS_PROXY_URL, window.location.origin).toString();
}
return CORS_PROXY_URL;
}
async function buildCorsProxyUrl(url: string): Promise<string> {
let proxyUrl = `${CORS_PROXY_URL}?url=${encodeURIComponent(url)}`;
let proxyUrl = `${resolveProxyBase()}?url=${encodeURIComponent(url)}`;
if (!CORS_PROXY_SECRET) {
return proxyUrl;
@@ -196,6 +209,9 @@ function createCorsAwareFetch(): {
? input.toString()
: input.url;
const isAlreadyProxied =
CORS_PROXY_URL.length > 0 && url.startsWith(CORS_PROXY_URL);
const isExternalCertificateUrl =
(url.includes('.crt') ||
url.includes('.cer') ||
@@ -221,6 +237,7 @@ function createCorsAwareFetch(): {
url.includes('RFC3161');
const shouldProxy =
!isAlreadyProxied &&
(isExternalCertificateUrl || isTsaRequest) &&
!url.startsWith(window.location.origin);
@@ -332,6 +349,19 @@ export async function timestampPdf(
pdfBytes: Uint8Array,
tsaUrl: string
): Promise<Uint8Array> {
const pageIsHttps =
typeof window !== 'undefined' && window.location?.protocol === 'https:';
const tsaIsHttp = /^http:\/\//i.test(tsaUrl);
if (pageIsHttps && tsaIsHttp && !CORS_PROXY_URL) {
throw new Error(
`This TSA endpoint uses HTTP (${tsaUrl}). The browser blocks insecure ` +
`requests from this HTTPS page. Either choose a TSA with an HTTPS ` +
`endpoint or configure VITE_CORS_PROXY_URL at build time so the ` +
`request can be relayed through your proxy.`
);
}
let effectiveUrl = tsaUrl;
if (CORS_PROXY_URL) {
+4 -9
View File
@@ -1,5 +1,6 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { t } from '../i18n/i18n';
import {
readFileAsArrayBuffer,
formatBytes,
@@ -197,13 +198,7 @@ function updateProcessButton(): void {
const processBtn = getElement<HTMLButtonElement>('process-btn');
if (!processBtn) return;
if (state.pdfBytes) {
processBtn.classList.remove('hidden');
processBtn.disabled = false;
} else {
processBtn.classList.add('hidden');
processBtn.disabled = true;
}
processBtn.disabled = !state.pdfBytes;
}
function getTsaUrl(): string | null {
@@ -233,8 +228,8 @@ async function processTimestamp(): Promise<void> {
downloadFile(blob, state.pdfFile.name);
showAlert(
'Success',
'PDF timestamped successfully! The timestamp can be verified in Adobe Acrobat and other PDF readers.',
t('common.success'),
t('tools:timestampPdf.successMessage'),
'success'
);
+19 -19
View File
@@ -194,41 +194,41 @@
class="w-full bg-gray-700 border border-gray-600 text-gray-200 rounded-lg px-3 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
></select>
</div>
</div>
<button
id="process-btn"
class="hidden w-full mt-6 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2"
disabled
>
<i data-lucide="clock" class="w-5 h-5"></i>
<span data-i18n="tools:timestampPdf.applyTimestamp"
>Apply Timestamp</span
<button
id="process-btn"
class="btn-gradient w-full mt-6 disabled:opacity-50 disabled:cursor-not-allowed"
disabled
data-i18n="tools:timestampPdf.applyTimestamp"
>
</button>
Apply Timestamp
</button>
</div>
</div>
</div>
<!-- Loader Modal -->
<div
id="loader-overlay"
class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
id="loader-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
>
<div class="text-center">
<div
class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-400 border-t-transparent mb-4"
></div>
<div
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
>
<div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium">
Processing...
</p>
</div>
</div>
<!-- Alert Modal -->
<div
id="alert-overlay"
class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center"
id="alert-modal"
class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden"
>
<div
class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-md w-full mx-4 border border-gray-700"
class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700"
>
<h3
id="alert-title"
+38
View File
@@ -115,4 +115,42 @@ describe('timestampPdf', () => {
expect(samplePdfBytes).toEqual(originalCopy);
});
it('should pre-proxy the TSA URL when CORS proxy is configured', async () => {
vi.stubEnv(
'VITE_CORS_PROXY_URL',
'https://bentopdf-cors-proxy.bentopdf.workers.dev'
);
vi.resetModules();
const { timestampPdf: freshTimestamp } =
await import('@/js/logic/digital-sign-pdf');
mockSign.mockResolvedValueOnce(new Uint8Array([1]));
await freshTimestamp(samplePdfBytes, 'http://timestamp.digicert.com');
const callArg = vi.mocked(PdfSigner).mock.calls[0][0] as {
signdate: { url: string };
};
expect(callArg.signdate.url).toMatch(
/^https:\/\/bentopdf-cors-proxy\.bentopdf\.workers\.dev\?url=/
);
expect(callArg.signdate.url).toContain(
encodeURIComponent('http://timestamp.digicert.com')
);
});
it('should throw a clear error when HTTPS page targets HTTP TSA without proxy', async () => {
vi.stubEnv('VITE_CORS_PROXY_URL', '');
Object.defineProperty(window, 'location', {
configurable: true,
value: { protocol: 'https:', origin: 'https://www.bentopdf.com' },
});
vi.resetModules();
const { timestampPdf: freshTimestamp } =
await import('@/js/logic/digital-sign-pdf');
await expect(
freshTimestamp(samplePdfBytes, 'http://timestamp.digicert.com')
).rejects.toThrow(/HTTPS page|VITE_CORS_PROXY_URL/);
});
});
+13 -6
View File
@@ -149,12 +149,14 @@ describe('TimestampNode', () => {
expect(result.pdf[0].filename).toBe('report_timestamped.pdf');
});
it('should wrap errors from timestampPdf with TSA context', async () => {
vi.mocked(timestampPdf).mockRejectedValueOnce(new Error('Network error'));
it('should wrap errors from timestampPdf with TSA context and original cause', async () => {
const originalError = new Error('Network error');
vi.mocked(timestampPdf).mockRejectedValueOnce(originalError);
const node = new TimestampNode();
await expect(
node.data({
let caught: Error | null = null;
try {
await node.data({
pdf: [
{
type: 'pdf' as const,
@@ -163,7 +165,12 @@ describe('TimestampNode', () => {
filename: 'test.pdf',
},
],
})
).rejects.toThrow(/Failed to timestamp using TSA/);
});
} catch (err) {
caught = err as Error;
}
expect(caught).toBeInstanceOf(Error);
expect(caught?.cause).toBe(originalError);
});
});