From a6510615549330c6bb2ea3be74a3b87c7463a6e9 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Fri, 27 Mar 2026 15:47:44 +0000 Subject: [PATCH] feat(sign): merge cert-sign and wet-sign into a unified Sign tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combines the separate CertSign and Sign tools into a single "Sign" entry that mirrors the shared-signing participant view. Users pick wet sign, cert sign, or both from a segmented control at the top of the full-screen editor. New files: - CombinedSignEditor.tsx — full-screen editor with mode selector, SignControlsStrip for wet/both modes, LocalEmbedPDFWithAnnotations, and CertificateConfigModal (with optional appearance settings) - CombinedSign.tsx — tool entry point; registers editor as a custom workbench view (hideToolPanel: true) and opens it on demand - useCombinedSignOperation.ts — submitCertSign (cert-sign endpoint) and submitWetOnly (sequential add-signature calls) Modified files: - CertificateConfigModal — adds showAppearanceSettings prop that renders SignatureSettingsInput and passes appearance data to onSign - toolId.ts — removes certSign from CORE_REGULAR_TOOL_IDS - useTranslatedToolRegistry — removes certSign entry; sign entry now uses CombinedSign with endpoints ["sign", "cert-sign"] - urlMapping.ts — /cert-sign and /manage-signatures redirect to sign - SignPopout.tsx — quick-access cert-sign button now selects sign - creditCosts.ts — merges certSign credit cost into sign (LARGE) - translation.toml — adds sign.editor.* and sign.mode.* keys --- .../public/locales/en-GB/translation.toml | 17 +- .../components/shared/signing/SignPopout.tsx | 6 +- .../modals/CertificateConfigModal.tsx | 50 ++- .../tools/combinedSign/CombinedSignEditor.tsx | 332 ++++++++++++++++++ .../core/data/useTranslatedToolRegistry.tsx | 29 +- .../combinedSign/useCombinedSignOperation.ts | 78 ++++ frontend/src/core/tools/CombinedSign.tsx | 133 +++++++ frontend/src/core/types/toolId.ts | 1 - frontend/src/core/utils/urlMapping.ts | 4 +- frontend/src/proprietary/utils/creditCosts.ts | 3 +- 10 files changed, 614 insertions(+), 39 deletions(-) create mode 100644 frontend/src/core/components/tools/combinedSign/CombinedSignEditor.tsx create mode 100644 frontend/src/core/hooks/tools/combinedSign/useCombinedSignOperation.ts create mode 100644 frontend/src/core/tools/CombinedSign.tsx diff --git a/frontend/public/locales/en-GB/translation.toml b/frontend/public/locales/en-GB/translation.toml index 94d8a6f94e..bfff12ce21 100644 --- a/frontend/public/locales/en-GB/translation.toml +++ b/frontend/public/locales/en-GB/translation.toml @@ -4157,8 +4157,8 @@ tags = "javascript,code,script,show javascript,show JS,find javascript,detect ja title = "Show Javascript" [home.sign] -desc = "Adds signature to PDF by drawing, text or image" -tags = "signature,autograph,e-sign,electronic signature,digital signature,sign document,approval,signoff,authorize,endorse,ink signature,handwriting" +desc = "Sign PDFs with a wet signature, digital certificate, or both" +tags = "signature,autograph,e-sign,electronic signature,digital signature,sign document,approval,signoff,authorize,endorse,ink signature,handwriting,certificate sign,cert sign,wet sign" title = "Sign" [home.timestampPdf] @@ -6916,11 +6916,24 @@ saved = "Select a saved signature above, then click anywhere on the PDF to place text = "After entering your name above, click anywhere on the PDF to place your signature." title = "How to add signature" +[sign.editor] +back = "Back to file selection" +hint = "Open the editor to place wet signatures, apply a digital certificate, or both." +noSignatures = "Place at least one signature on the PDF first." +open = "Open Sign Editor" +openStep = "Open Sign Editor" +signingFailed = "Signing failed. Please try again." +signDocument = "Sign Document" +title = "Sign Editor" + [sign.mode] +both = "Both" +cert = "Cert Sign" move = "Move Signature" pause = "Pause placement" place = "Place Signature" resume = "Resume placement" +wet = "Wet Sign" [sign.results] title = "Signature Results" diff --git a/frontend/src/core/components/shared/signing/SignPopout.tsx b/frontend/src/core/components/shared/signing/SignPopout.tsx index 183f731831..e6697b7519 100644 --- a/frontend/src/core/components/shared/signing/SignPopout.tsx +++ b/frontend/src/core/components/shared/signing/SignPopout.tsx @@ -677,11 +677,11 @@ const SignPopout = ({ isOpen, onClose, buttonRef, isRTL, groupSigningEnabled }: className="quick-access-popout__quick-sign-btn" onClick={() => { onClose(); - handleToolSelect('certSign'); + handleToolSelect('sign'); }} > - - {t('quickAccess.certSign', 'Certificate Sign')} + + {t('quickAccess.sign', 'Sign')} diff --git a/frontend/src/core/components/tools/certSign/modals/CertificateConfigModal.tsx b/frontend/src/core/components/tools/certSign/modals/CertificateConfigModal.tsx index 373ad8df72..26cb6144aa 100644 --- a/frontend/src/core/components/tools/certSign/modals/CertificateConfigModal.tsx +++ b/frontend/src/core/components/tools/certSign/modals/CertificateConfigModal.tsx @@ -1,7 +1,8 @@ -import { Modal, Stack, Group, Button, Text, Collapse, TextInput } from '@mantine/core'; +import { Modal, Stack, Group, Button, Text, Collapse, TextInput, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; import { CertificateSelector, CertificateType, UploadFormat } from '@app/components/tools/certSign/CertificateSelector'; +import SignatureSettingsInput, { SignatureSettings } from '@app/components/tools/certSign/SignatureSettingsInput'; export interface CertificateSubmitData { certType: CertificateType; @@ -11,6 +12,11 @@ export interface CertificateSubmitData { certFile: File | null; jksFile: File | null; password: string; + // Appearance (only present when showAppearanceSettings=true) + showSignature?: boolean; + pageNumber?: number; + showLogo?: boolean; + includeSummaryPage?: boolean; } interface CertificateConfigModalProps { @@ -21,6 +27,8 @@ interface CertificateConfigModalProps { disabled?: boolean; defaultReason?: string; defaultLocation?: string; + /** When true, renders SignatureSettingsInput so the user can configure cert appearance */ + showAppearanceSettings?: boolean; } export const CertificateConfigModal: React.FC = ({ @@ -31,6 +39,7 @@ export const CertificateConfigModal: React.FC = ({ disabled = false, defaultReason = '', defaultLocation = '', + showAppearanceSettings = false, }) => { const { t } = useTranslation(); @@ -48,6 +57,11 @@ export const CertificateConfigModal: React.FC = ({ const [reason, setReason] = useState(defaultReason); const [location, setLocation] = useState(defaultLocation); + // Appearance settings (only used when showAppearanceSettings=true) + const [appearance, setAppearance] = useState({ + showSignature: false, + }); + const isUploadValid = () => { if (certType !== 'UPLOAD') return true; switch (uploadFormat) { @@ -71,11 +85,22 @@ export const CertificateConfigModal: React.FC = ({ setSigning(true); try { - await onSign( - { certType, uploadFormat, p12File, privateKeyFile, certFile, jksFile, password }, - reason, - location - ); + const certData: CertificateSubmitData = { + certType, + uploadFormat, + p12File, + privateKeyFile, + certFile, + jksFile, + password, + ...(showAppearanceSettings && { + showSignature: appearance.showSignature, + pageNumber: appearance.pageNumber, + showLogo: appearance.showLogo, + includeSummaryPage: appearance.includeSummaryPage, + }), + }; + await onSign(certData, reason, location); } catch (error) { console.error('Failed to sign document:', error); } finally { @@ -118,6 +143,19 @@ export const CertificateConfigModal: React.FC = ({ disabled={disabled || signing} /> + {/* Signature Appearance Settings (combined sign tool) */} + {showAppearanceSettings && ( + <> + + + + + )} + {/* Advanced Settings - Optional */}
+ + + + + )} + + {/* Cert-only mode: show sign button in the top bar (no strip below) */} + {signMode === 'cert' && ( + <> + + + + )} + + + + + + + + {/* ── Wet signature controls strip (wet / both modes) ─────────────── */} + {showWetControls && ( + 0} + signatureConfig={signatureConfig} + hasSelectedAnnotation={hasSelectedAnnotation} + onDeleteSelected={handleDeleteSelected} + /> + )} + + {/* ── PDF viewer ──────────────────────────────────────────────────── */} +
+ {}} + placementMode={showWetControls ? placementMode : false} + signatureData={showWetControls ? signatureConfig?.signatureData : undefined} + signatureType={showWetControls ? signatureConfig?.signatureType : undefined} + onPlaceSignature={() => {}} + onPreviewCountChange={setPreviewCount} + /> +
+ + {/* ── Certificate modal (cert / both modes) ───────────────────────── */} + {(signMode === 'cert' || signMode === 'both') && ( + setCertModalOpen(false)} + onSign={handleCertSign} + signatureCount={previewCount} + disabled={loading} + showAppearanceSettings + /> + )} +
+ ); +}; + +export default CombinedSignEditor; diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index 3c021d0306..4ac4988aac 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -42,7 +42,7 @@ import UnlockPdfForms from "@app/tools/UnlockPdfForms"; import FormFill from "@app/tools/formFill/FormFill"; import RemoveCertificateSign from "@app/tools/RemoveCertificateSign"; import RemoveImage from "@app/tools/RemoveImage"; -import CertSign from "@app/tools/CertSign"; +import CombinedSign from "@app/tools/CombinedSign"; import TimestampPdf from "@app/tools/TimestampPdf"; import BookletImposition from "@app/tools/BookletImposition"; import Flatten from "@app/tools/Flatten"; @@ -50,7 +50,6 @@ import Rotate from "@app/tools/Rotate"; import PdfTextEditor from "@app/tools/pdfTextEditor/PdfTextEditor"; import ChangeMetadata from "@app/tools/ChangeMetadata"; import Crop from "@app/tools/Crop"; -import Sign from "@app/tools/Sign"; import AddText from "@app/tools/AddText"; import AddImage from "@app/tools/AddImage"; import Annotate from "@app/tools/Annotate"; @@ -69,7 +68,6 @@ import { ocrOperationConfig } from "@app/hooks/tools/ocr/useOCROperation"; import { convertOperationConfig } from "@app/hooks/tools/convert/useConvertOperation"; import { removeCertificateSignOperationConfig } from "@app/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; import { changePermissionsOperationConfig } from "@app/hooks/tools/changePermissions/useChangePermissionsOperation"; -import { certSignOperationConfig } from "@app/hooks/tools/certSign/useCertSignOperation"; import { timestampPdfOperationConfig } from "@app/hooks/tools/timestampPdf/useTimestampPdfOperation"; import { bookletImpositionOperationConfig } from "@app/hooks/tools/bookletImposition/useBookletImpositionOperation"; import { mergeOperationConfig } from '@app/hooks/tools/merge/useMergeOperation'; @@ -113,7 +111,6 @@ import MergeSettings from '@app/components/tools/merge/MergeSettings'; import AdjustPageScaleSettings from "@app/components/tools/adjustPageScale/AdjustPageScaleSettings"; import ScannerImageSplitSettings from "@app/components/tools/scannerImageSplit/ScannerImageSplitSettings"; import ChangeMetadataSingleStep from "@app/components/tools/changeMetadata/ChangeMetadataSingleStep"; -import SignSettings from "@app/components/tools/sign/SignSettings"; import AddPageNumbers from "@app/tools/AddPageNumbers"; import RemoveAnnotations from "@app/tools/RemoveAnnotations"; import PageLayoutSettings from "@app/components/tools/pageLayout/PageLayoutSettings"; @@ -123,7 +120,6 @@ import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractIm import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings"; import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings"; import AddStampAutomationSettings from "@app/components/tools/addStamp/AddStampAutomationSettings"; -import CertSignAutomationSettings from "@app/components/tools/certSign/CertSignAutomationSettings"; import CropAutomationSettings from "@app/components/tools/crop/CropAutomationSettings"; import RotateAutomationSettings from "@app/components/tools/rotate/RotateAutomationSettings"; import SplitAutomationSettings from "@app/components/tools/split/SplitAutomationSettings"; @@ -202,19 +198,6 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "merge") }, // Signing - certSign: { - icon: , - name: t("home.certSign.title", "Certificate Sign"), - component: CertSign, - description: t("home.certSign.desc", "Sign PDF documents using digital certificates"), - categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.SIGNING, - synonyms: getSynonyms(t, "certSign"), - maxFiles: -1, - endpoints: ["cert-sign"], - operationConfig: certSignOperationConfig, - automationSettings: CertSignAutomationSettings, - }, timestampPdf: { icon: , name: t("home.timestampPdf.title", "Timestamp PDF"), @@ -231,15 +214,15 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { sign: { icon: , name: t("home.sign.title", "Sign"), - component: Sign, - description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), + component: CombinedSign, + description: t("home.sign.desc", "Sign PDFs with a wet signature, digital certificate, or both"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.SIGNING, - endpoints: ["sign"], + endpoints: ["sign", "cert-sign"], operationConfig: signOperationConfig, - automationSettings: SignSettings, // TODO:: not all settings shown, suggested next tools shown + automationSettings: null, synonyms: getSynonyms(t, "sign"), - supportsAutomate: false, //TODO make support Sign + supportsAutomate: false, }, addText: { icon: , diff --git a/frontend/src/core/hooks/tools/combinedSign/useCombinedSignOperation.ts b/frontend/src/core/hooks/tools/combinedSign/useCombinedSignOperation.ts new file mode 100644 index 0000000000..26b39906ad --- /dev/null +++ b/frontend/src/core/hooks/tools/combinedSign/useCombinedSignOperation.ts @@ -0,0 +1,78 @@ +import { useState, useCallback } from 'react'; +import apiClient from '@app/services/apiClient'; +import type { SignaturePreview } from '@app/components/viewer/LocalEmbedPDFWithAnnotations'; + +export interface CombinedSignResult { + blob: Blob; + filename: string; +} + +export interface UseCombinedSignOperationReturn { + submitCertSign: (file: File, formData: FormData) => Promise; + submitWetOnly: (file: File, previews: SignaturePreview[]) => Promise; + loading: boolean; +} + +/** + * Operation hook for the combined sign tool. + * + * - submitCertSign: posts a pre-built FormData to /api/v1/security/cert-sign and + * returns the signed PDF blob. + * - submitWetOnly: sequentially applies each wet-signature preview via + * /api/v1/security/add-signature (one call per signature, chained). + * + * Coordinates from LocalEmbedPDFWithAnnotations are normalised 0-1 fractions; + * both endpoints are expected to accept this format. + */ +export const useCombinedSignOperation = (): UseCombinedSignOperationReturn => { + const [loading, setLoading] = useState(false); + + const extractFilename = (headers: any, fallback: string): string => { + const disposition: string = headers['content-disposition'] ?? ''; + const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition); + if (match?.[1]) return match[1].replace(/['"]/g, ''); + return fallback; + }; + + const submitCertSign = useCallback(async (file: File, formData: FormData): Promise => { + setLoading(true); + try { + const response = await apiClient.post('/api/v1/security/cert-sign', formData, { + responseType: 'blob', + }); + const blob = new Blob([response.data], { type: 'application/pdf' }); + return { blob, filename: extractFilename(response.headers, `signed_${file.name}`) }; + } finally { + setLoading(false); + } + }, []); + + const submitWetOnly = useCallback(async (file: File, previews: SignaturePreview[]): Promise => { + setLoading(true); + try { + let currentFile: File = file; + for (const preview of previews) { + const formData = new FormData(); + formData.append('fileInput', currentFile); + formData.append('signatureData', preview.signatureData); + formData.append('signatureType', preview.signatureType); + formData.append('x', preview.x.toString()); + formData.append('y', preview.y.toString()); + formData.append('width', preview.width.toString()); + formData.append('height', preview.height.toString()); + formData.append('page', preview.pageIndex.toString()); + + const response = await apiClient.post('/api/v1/security/add-signature', formData, { + responseType: 'blob', + }); + const blob = new Blob([response.data], { type: 'application/pdf' }); + currentFile = new File([blob], file.name, { type: 'application/pdf' }); + } + return { blob: currentFile, filename: `signed_${file.name}` }; + } finally { + setLoading(false); + } + }, []); + + return { submitCertSign, submitWetOnly, loading }; +}; diff --git a/frontend/src/core/tools/CombinedSign.tsx b/frontend/src/core/tools/CombinedSign.tsx new file mode 100644 index 0000000000..2df37b25bc --- /dev/null +++ b/frontend/src/core/tools/CombinedSign.tsx @@ -0,0 +1,133 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Stack, Text } from '@mantine/core'; +import OpenInFullIcon from '@mui/icons-material/OpenInFull'; + +import { createToolFlow } from '@app/components/tools/shared/createToolFlow'; +import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool'; +import { BaseToolProps, ToolComponent } from '@app/types/tool'; +import { useSignParameters, DEFAULT_PARAMETERS } from '@app/hooks/tools/sign/useSignParameters'; +import { useSignOperation, signOperationConfig } from '@app/hooks/tools/sign/useSignOperation'; +import { useToolWorkflow } from '@app/contexts/ToolWorkflowContext'; +import { useNavigationActions, useNavigationState } from '@app/contexts/NavigationContext'; +import CombinedSignEditor, { CombinedSignEditorData } from '@app/components/tools/combinedSign/CombinedSignEditor'; +import { LocalIcon } from '@app/components/shared/LocalIcon'; + +const EDITOR_VIEW_ID = 'combinedSignEditor'; +const EDITOR_WORKBENCH_ID = 'custom:combinedSignEditor' as const; + +const CombinedSign = (props: BaseToolProps) => { + const { t } = useTranslation(); + const { actions: navigationActions } = useNavigationActions(); + const navigationState = useNavigationState(); + const { + registerCustomWorkbenchView, + unregisterCustomWorkbenchView, + setCustomWorkbenchViewData, + clearCustomWorkbenchViewData, + } = useToolWorkflow(); + + const base = useBaseTool('sign', useSignParameters, useSignOperation, props); + + const editorIcon = useMemo( + () => , + [], + ); + + // Register the full-screen editor as a custom workbench view (once on mount) + useEffect(() => { + registerCustomWorkbenchView({ + id: EDITOR_VIEW_ID, + workbenchId: EDITOR_WORKBENCH_ID, + label: t('sign.editor.title', 'Sign Editor'), + icon: editorIcon, + component: CombinedSignEditor, + hideToolPanel: true, + }); + return () => { + clearCustomWorkbenchViewData(EDITOR_VIEW_ID); + unregisterCustomWorkbenchView(EDITOR_VIEW_ID); + }; + }, [clearCustomWorkbenchViewData, editorIcon, registerCustomWorkbenchView, t, unregisterCustomWorkbenchView]); + + const handleOpenEditor = useCallback(() => { + if (base.selectedFiles.length === 0) return; + + const file = base.selectedFiles[0]; // StirlingFile extends File — works directly + + const editorData: CombinedSignEditorData = { + file, + onComplete: (blob, filename) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + navigationActions.setWorkbench('viewer'); + }, + onBack: () => { + navigationActions.setWorkbench('viewer'); + }, + }; + + setCustomWorkbenchViewData(EDITOR_VIEW_ID, editorData); + navigationActions.setWorkbench(EDITOR_WORKBENCH_ID); + }, [base.selectedFiles, navigationActions, setCustomWorkbenchViewData]); + + const isEditorOpen = navigationState.workbench === EDITOR_WORKBENCH_ID; + + return createToolFlow({ + files: { + selectedFiles: base.selectedFiles, + isCollapsed: isEditorOpen, + }, + steps: base.selectedFiles.length > 0 + ? [ + { + title: t('sign.editor.openStep', 'Open Sign Editor'), + isCollapsed: false, + content: ( + + + {t( + 'sign.editor.hint', + 'Open the editor to place wet signatures, apply a digital certificate, or both.', + )} + + + + ), + }, + ] + : [], + executeButton: { + text: t('sign.editor.open', 'Open Sign Editor'), + loadingText: t('loading', 'Loading...'), + onClick: async () => handleOpenEditor(), + isVisible: false, + endpointEnabled: base.endpointEnabled, + paramsValid: true, + }, + review: { + isVisible: false, + operation: base.operation, + title: '', + onUndo: base.handleUndo, + }, + }); +}; + +const CombinedSignTool = CombinedSign as ToolComponent; +CombinedSignTool.tool = () => useSignOperation; +CombinedSignTool.getDefaultParameters = () => ({ ...DEFAULT_PARAMETERS }); + +export default CombinedSignTool; diff --git a/frontend/src/core/types/toolId.ts b/frontend/src/core/types/toolId.ts index 44103b6548..cdcac924c8 100644 --- a/frontend/src/core/types/toolId.ts +++ b/frontend/src/core/types/toolId.ts @@ -7,7 +7,6 @@ import { export type ToolKind = 'regular' | 'super' | 'link'; export const CORE_REGULAR_TOOL_IDS = [ - 'certSign', 'sign', 'addText', 'addPassword', diff --git a/frontend/src/core/utils/urlMapping.ts b/frontend/src/core/utils/urlMapping.ts index 118426ddfe..036f4b9c7c 100644 --- a/frontend/src/core/utils/urlMapping.ts +++ b/frontend/src/core/utils/urlMapping.ts @@ -38,8 +38,8 @@ export const URL_TO_TOOL_MAP: Record = { '/add-password': 'addPassword', '/remove-password': 'removePassword', '/change-permissions': 'changePermissions', - '/cert-sign': 'certSign', - '/manage-signatures': 'certSign', + '/cert-sign': 'sign', + '/manage-signatures': 'sign', '/remove-certificate-sign': 'removeCertSign', '/remove-cert-sign': 'removeCertSign', '/unlock-pdf-forms': 'unlockPDFForms', diff --git a/frontend/src/proprietary/utils/creditCosts.ts b/frontend/src/proprietary/utils/creditCosts.ts index f87315f2bb..74b8a4303b 100644 --- a/frontend/src/proprietary/utils/creditCosts.ts +++ b/frontend/src/proprietary/utils/creditCosts.ts @@ -37,7 +37,7 @@ export const TOOL_CREDIT_COSTS: Record = { reorganizePages: CREDIT_COSTS.SMALL, scalePages: CREDIT_COSTS.SMALL, editTableOfContents: CREDIT_COSTS.SMALL, - sign: CREDIT_COSTS.SMALL, + sign: CREDIT_COSTS.LARGE, removeAnnotations: CREDIT_COSTS.SMALL, removeImage: CREDIT_COSTS.SMALL, scannerImageSplit: CREDIT_COSTS.SMALL, @@ -76,7 +76,6 @@ export const TOOL_CREDIT_COSTS: Record = { compress: CREDIT_COSTS.LARGE, convert: CREDIT_COSTS.LARGE, ocr: CREDIT_COSTS.LARGE, - certSign: CREDIT_COSTS.LARGE, timestampPdf: CREDIT_COSTS.LARGE, // Extra large operations (10 credits)