diff --git a/apps/remix/app/components/general/app-command-menu.tsx b/apps/remix/app/components/general/app-command-menu.tsx index 2e42b5c4e2..7a53ff2492 100644 --- a/apps/remix/app/components/general/app-command-menu.tsx +++ b/apps/remix/app/components/general/app-command-menu.tsx @@ -599,18 +599,10 @@ export const AppCommandMenu = ({ open, onOpenChange }: AppCommandMenuProps) => { isVisibleCountCapped ? ( {formatChipCount(totalVisibleCount, isVisibleCountCapped)} results ) : ( - + ) ) : ( - + )} diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx index 17a5c421c0..f0cbfe3259 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx @@ -11,13 +11,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { ArrowLeftIcon, KeyIcon, MailIcon, RotateCwIcon } from 'lucide-react'; +import { KeyIcon, MailIcon, RotateCwIcon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { EnableAuthenticatorAppDialog } from '~/components/forms/2fa/enable-authenticator-app-dialog'; - import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; export type DocumentSigningAuth2FAProps = { @@ -89,9 +87,7 @@ export const DocumentSigningAuth2FA = ({ toast({ title: _(msg`Verification Code Sent`), - description: _( - msg`A 6-digit verification code has been sent to ${recipient.email}. Please check your inbox.`, - ), + description: _(msg`A 6-digit verification code has been sent to ${recipient.email}. Please check your inbox.`), }); } catch (err) { setIsSendingEmail(false); @@ -146,12 +142,12 @@ export const DocumentSigningAuth2FA = ({
{/* Method switcher if user has both options */} {user?.twoFactorEnabled && ( -
+
-
- - - - - Download - - } - /> -
+
+ + + + + Download + + } + /> +
diff --git a/apps/remix/app/components/general/organisation-usage-panel.tsx b/apps/remix/app/components/general/organisation-usage-panel.tsx index 99a69a6613..de83561965 100644 --- a/apps/remix/app/components/general/organisation-usage-panel.tsx +++ b/apps/remix/app/components/general/organisation-usage-panel.tsx @@ -170,10 +170,10 @@ const UsageStatCard = ({ label, icon: Icon, used, limit, countOnly = false, foot {hasFiniteLimit ? : null} - {subtext ?

{subtext}

: null} + {Boolean(subtext) ?

{subtext}

: null}
- {action ?
{action}
: null} + {Boolean(action) ?
{action}
: null} ); }; diff --git a/apps/remix/app/routes/_share+/share.$slug.tsx b/apps/remix/app/routes/_share+/share.$slug.tsx index dd18d3ae38..4e8029f73e 100644 --- a/apps/remix/app/routes/_share+/share.$slug.tsx +++ b/apps/remix/app/routes/_share+/share.$slug.tsx @@ -1,7 +1,6 @@ -import { redirect, useLoaderData } from 'react-router'; - import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { getDocumentByAccessToken } from '@documenso/lib/server-only/document/get-document-by-access-token'; +import { redirect, useLoaderData } from 'react-router'; import { DocumentCertificateQRView } from '~/components/general/document/document-certificate-qr-view'; diff --git a/apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx b/apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx index 2a31503333..c6476b4148 100644 --- a/apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx +++ b/apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx @@ -16,10 +16,10 @@ export default function SignatureDisclosure() {

- Thank you for using Crove Sign to perform your electronic document signing. The purpose of this disclosure is - to inform you about the process, legality, and your rights regarding the use of electronic signatures on our - platform. By opting to use an electronic signature, you are agreeing to the terms and conditions outlined - below. + Thank you for using Crove Sign to perform your electronic document signing. The purpose of this disclosure + is to inform you about the process, legality, and your rights regarding the use of electronic signatures on + our platform. By opting to use an electronic signature, you are agreeing to the terms and conditions + outlined below.

diff --git a/apps/remix/app/routes/_unauthenticated+/articles.verify-document.tsx b/apps/remix/app/routes/_unauthenticated+/articles.verify-document.tsx index e408673279..bfceaef5e3 100644 --- a/apps/remix/app/routes/_unauthenticated+/articles.verify-document.tsx +++ b/apps/remix/app/routes/_unauthenticated+/articles.verify-document.tsx @@ -98,12 +98,13 @@ export default function DocumentVerificationArticlePage() {
-

+

Document Integrity Verification

- Verify the cryptographic tamper-evident receipt and on-chain attestation of any signed document on DOS Chain / EAS. + Verify the cryptographic tamper-evident receipt and on-chain attestation of any signed document on DOS Chain + / EAS.

@@ -111,7 +112,7 @@ export default function DocumentVerificationArticlePage() { {/* Drag & Drop Upload Card */} - + Upload Final PDF @@ -132,7 +133,9 @@ export default function DocumentVerificationArticlePage() { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files?.[0]; - if (file) handleFileProcess(file); + if (file) { + handleFileProcess(file); + } }} onClick={() => { document.getElementById('verify-file-input')?.click(); @@ -145,14 +148,16 @@ export default function DocumentVerificationArticlePage() { className="hidden" onChange={(e) => { const file = e.target.files?.[0]; - if (file) handleFileProcess(file); + if (file) { + handleFileProcess(file); + } }} /> {isLoading ? (
-

+

Computing cryptographic hash and querying DOS Chain...

@@ -216,11 +221,13 @@ export default function DocumentVerificationArticlePage() {
- + Computed SHA-256 Hash
- {verificationResult.documentHash} + + {verificationResult.documentHash} +
{copiedField === 'hash' && ( - Copied! + + Copied! + )}
{verificationResult.attestationUid && (
- + EAS Attestation UID
- {verificationResult.attestationUid} + + {verificationResult.attestationUid} +
{copiedField === 'uid' && ( - Copied! + + Copied! + )}
)}
{verificationResult.isValid && ( -
-

+
+

Attestation Details

- Document Title: + + Document Title: +

{verificationResult.envelopeTitle || 'Document'}

- Anchored At: -

{verificationResult.anchoredAt ? new Date(verificationResult.anchoredAt).toLocaleString() : 'N/A'}

+ + Anchored At: + +

+ {verificationResult.anchoredAt + ? new Date(verificationResult.anchoredAt).toLocaleString() + : 'N/A'} +

- Network: + + Network: +

DOS Chain (EAS Layer)

{verificationResult.signers && verificationResult.signers.length > 0 && ( -
- Signers: +
+ + Signers: +
{verificationResult.signers.map((signer: any, idx: number) => (
- {signer.name} ({signer.email}) + + {signer.name} ({signer.email}) + {signer.role}
))} @@ -295,7 +322,10 @@ export default function DocumentVerificationArticlePage() { {/* Legal & Technical Disclaimer */}
- Disclaimer: {verificationResult.disclaimer} + + Disclaimer: + {' '} + {verificationResult.disclaimer}
diff --git a/apps/remix/server/api/blockchain/attestation-route.ts b/apps/remix/server/api/blockchain/attestation-route.ts index 38c7f786db..0ceac63d0b 100644 --- a/apps/remix/server/api/blockchain/attestation-route.ts +++ b/apps/remix/server/api/blockchain/attestation-route.ts @@ -1,7 +1,4 @@ -import { - verifyDocumentByQrToken, - verifyDocumentFile, -} from '@documenso/lib/server-only/blockchain/verify-attestation'; +import { verifyDocumentByQrToken, verifyDocumentFile } from '@documenso/lib/server-only/blockchain/verify-attestation'; import { prisma } from '@documenso/prisma'; import { Hono } from 'hono'; @@ -24,10 +21,7 @@ export const attestationRoute = new Hono() }); if (!anchor) { - return c.json( - { success: false, message: 'No blockchain anchor record found for this envelope' }, - 404, - ); + return c.json({ success: false, message: 'No blockchain anchor record found for this envelope' }, 404); } return c.json( diff --git a/apps/remix/server/api/webhooks/dos-webhook.ts b/apps/remix/server/api/webhooks/dos-webhook.ts index f99208cfc8..c5aa31c6da 100644 --- a/apps/remix/server/api/webhooks/dos-webhook.ts +++ b/apps/remix/server/api/webhooks/dos-webhook.ts @@ -72,122 +72,119 @@ const isDuplicateEvent = async (eventId: string): Promise => { return false; }; -export const dosWebhookRoute = new Hono() - .post('/dos-org-sync', async (c) => { - const signatureHeader = c.req.header('x-dos-signature') ?? null; - const webhookSecret = env('CROVE_DOS_WEBHOOK_SECRET') || env('NEXT_PRIVATE_DOS_WEBHOOK_SECRET') || ''; +export const dosWebhookRoute = new Hono().post('/dos-org-sync', async (c) => { + const signatureHeader = c.req.header('x-dos-signature') ?? null; + const webhookSecret = env('CROVE_DOS_WEBHOOK_SECRET') || env('NEXT_PRIVATE_DOS_WEBHOOK_SECRET') || ''; - const rawBody = await c.req.text(); + const rawBody = await c.req.text(); - let eventName = ''; + let eventName = ''; - try { - eventName = (JSON.parse(rawBody).event || '').toString().toLowerCase(); - } catch { - eventName = ''; - } + try { + eventName = (JSON.parse(rawBody).event || '').toString().toLowerCase(); + } catch { + eventName = ''; + } - const isPingEvent = - eventName === 'ping' || eventName === 'test' || eventName === 'endpoint.test'; - - // Fail-closed: the webhook handler performs privileged mutations - // (organisation deletion, member role grants), so it must never run - // without a configured shared secret. Only the connectivity ping is - // allowed through so the Developer Portal health check still reports - // that the endpoint is reachable but unconfigured. - if (!webhookSecret) { - if (isPingEvent) { - return c.json( - { - success: false, - message: - 'Webhook secret is not configured; events will be rejected. Set CROVE_DOS_WEBHOOK_SECRET.', - eventId: 'unconfigured', - }, - 200, - ); - } + const isPingEvent = eventName === 'ping' || eventName === 'test' || eventName === 'endpoint.test'; + // Fail-closed: the webhook handler performs privileged mutations + // (organisation deletion, member role grants), so it must never run + // without a configured shared secret. Only the connectivity ping is + // allowed through so the Developer Portal health check still reports + // that the endpoint is reachable but unconfigured. + if (!webhookSecret) { + if (isPingEvent) { return c.json( { success: false, - message: 'Webhook endpoint is not configured to process events', + message: 'Webhook secret is not configured; events will be rejected. Set CROVE_DOS_WEBHOOK_SECRET.', + eventId: 'unconfigured', }, - 503, + 200, ); } - const isValid = verifyDosWebhookSignature({ - rawBody, - signatureHeader, - secret: webhookSecret, - }); + return c.json( + { + success: false, + message: 'Webhook endpoint is not configured to process events', + }, + 503, + ); + } + + const isValid = verifyDosWebhookSignature({ + rawBody, + signatureHeader, + secret: webhookSecret, + }); + + if (!isValid) { + return c.json({ success: false, message: 'Invalid webhook signature' }, 401); + } + + try { + const payload = JSON.parse(rawBody); + const eventId = + payload.id || + payload.event_id || + payload.eventId || + crypto.createHash('sha256').update(rawBody).digest('hex').slice(0, 32); + + console.log(`[DOS Webhook] Received event: ${eventName} (id: ${eventId})`); + + // Immediate synchronous response for ping/test events from Developer Portal + if (eventName === 'ping' || eventName === 'test' || eventName === 'endpoint.test') { + return c.json( + { + success: true, + message: 'Pong! Webhook endpoint is active and verified.', + eventId, + }, + 200, + ); + } - if (!isValid) { - return c.json({ success: false, message: 'Invalid webhook signature' }, 401); + // Idempotency check: avoid duplicate execution for identical payloads in a 10-minute window + if (await isDuplicateEvent(eventId)) { + return c.json( + { + success: true, + message: 'Webhook event already processed (idempotent)', + eventId, + }, + 200, + ); } - try { - const payload = JSON.parse(rawBody); - const eventId = - payload.id || - payload.event_id || - payload.eventId || - crypto.createHash('sha256').update(rawBody).digest('hex').slice(0, 32); - - console.log(`[DOS Webhook] Received event: ${eventName} (id: ${eventId})`); - - // Immediate synchronous response for ping/test events from Developer Portal - if (eventName === 'ping' || eventName === 'test' || eventName === 'endpoint.test') { - return c.json( - { - success: true, - message: 'Pong! Webhook endpoint is active and verified.', - eventId, - }, - 200, - ); - } - - // Idempotency check: avoid duplicate execution for identical payloads in a 10-minute window - if (await isDuplicateEvent(eventId)) { - return c.json( - { - success: true, - message: 'Webhook event already processed (idempotent)', - eventId, - }, - 200, - ); - } - - const jobsProvider = env('NEXT_PRIVATE_JOBS_PROVIDER'); - - // If BullMQ or Inngest queue provider is configured, dispatch via persistent queue - if (jobsProvider === 'bullmq' || jobsProvider === 'inngest') { - await jobsClient.triggerJob({ - name: 'internal.process-dos-webhook', - payload: { - ...payload, - eventId, - }, - }); - - return c.json( - { - success: true, - message: 'Webhook event received and enqueued for persistent processing', - eventId, - }, - 200, - ); - } - - // Synchronous execution fallback (local / dev mode) - const result = await handleDosWebhookEvent(payload); - return c.json(result, result.success ? 200 : 400); - } catch (error) { - console.error('[DOS Webhook] Error processing payload:', error); - return c.json({ success: false, message: 'Internal Server Error' }, 500); + const jobsProvider = env('NEXT_PRIVATE_JOBS_PROVIDER'); + + // If BullMQ or Inngest queue provider is configured, dispatch via persistent queue + if (jobsProvider === 'bullmq' || jobsProvider === 'inngest') { + await jobsClient.triggerJob({ + name: 'internal.process-dos-webhook', + payload: { + ...payload, + eventId, + }, + }); + + return c.json( + { + success: true, + message: 'Webhook event received and enqueued for persistent processing', + eventId, + }, + 200, + ); } - }); + + // Synchronous execution fallback (local / dev mode) + const result = await handleDosWebhookEvent(payload); + return c.json(result, result.success ? 200 : 400); + } catch (error) { + console.error('[DOS Webhook] Error processing payload:', error); + return c.json({ success: false, message: 'Internal Server Error' }, 500); + } +}); diff --git a/packages/app-tests/e2e/dos-id/dos-oidc-auth.spec.ts b/packages/app-tests/e2e/dos-id/dos-oidc-auth.spec.ts index edef1116b2..4af5caae69 100644 --- a/packages/app-tests/e2e/dos-id/dos-oidc-auth.spec.ts +++ b/packages/app-tests/e2e/dos-id/dos-oidc-auth.spec.ts @@ -28,7 +28,7 @@ test.describe('[DOS OIDC Auth]: Sign In & Sign Up pages', () => { test('should handle invalid OIDC callback gracefully', async ({ page }) => { // Navigate to OIDC callback with invalid state/code const response = await page.goto('/api/auth/callback/oidc?code=invalid_mock_code&state=invalid_state'); - + // Should not crash the server (either redirect to /signin with error or return 400/500 structured response) expect(response?.status()).toBeLessThan(600); }); diff --git a/packages/app-tests/e2e/dos-id/dos-webhook-sync.spec.ts b/packages/app-tests/e2e/dos-id/dos-webhook-sync.spec.ts index 8a7364aff9..5c327ab21c 100644 --- a/packages/app-tests/e2e/dos-id/dos-webhook-sync.spec.ts +++ b/packages/app-tests/e2e/dos-id/dos-webhook-sync.spec.ts @@ -3,8 +3,9 @@ import { prisma } from '@documenso/prisma'; import { expect, test } from '@playwright/test'; test.describe('[DOS Webhook Sync]: /api/webhooks/dos-org-sync', () => { - const webhookSecret = process.env.CROVE_DOS_WEBHOOK_SECRET || process.env.NEXT_PRIVATE_DOS_WEBHOOK_SECRET || 'test-dos-webhook-secret'; - + const webhookSecret = + process.env.CROVE_DOS_WEBHOOK_SECRET || process.env.NEXT_PRIVATE_DOS_WEBHOOK_SECRET || 'test-dos-webhook-secret'; + const generateSignature = (body: string, secret: string) => { return `sha256=${crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')}`; }; diff --git a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts index 875d4ea6bc..f980406d79 100644 --- a/packages/auth/server/lib/utils/handle-oauth-callback-url.ts +++ b/packages/auth/server/lib/utils/handle-oauth-callback-url.ts @@ -5,7 +5,11 @@ import { isSignupEnabledForProvider, } from '@documenso/lib/constants/auth'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { syncDosProfileAndOrgs, type DosOrgClaim, type DosTeamClaim } from '@documenso/lib/server-only/dos-id/sync-dos-profile'; +import { + type DosOrgClaim, + type DosTeamClaim, + syncDosProfileAndOrgs, +} from '@documenso/lib/server-only/dos-id/sync-dos-profile'; import { getEmailBlocklistDomains } from '@documenso/lib/server-only/site-settings/get-email-blocklist-domains'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account'; @@ -32,7 +36,19 @@ export const handleOAuthCallbackUrl = async (options: HandleOAuthCallbackUrlOpti const requestMeta = c.get('requestMetadata'); - const { email, name, sub, accessToken, accessTokenExpiresAt, idToken, redirectPath, avatarUrl, organizations, teams, activeOrgId } = await validateOauth({ + const { + email, + name, + sub, + accessToken, + accessTokenExpiresAt, + idToken, + redirectPath, + avatarUrl, + organizations, + teams, + activeOrgId, + } = await validateOauth({ c, clientOptions, }); @@ -295,12 +311,25 @@ export const validateOauth = async (options: HandleOAuthCallbackUrlOptions) => { const email = claims.email; const name = claims.name || claims.display_name; const sub = claims.sub; - const avatarUrl = (typeof claims.picture === 'string' ? claims.picture : typeof claims.avatar_url === 'string' ? claims.avatar_url : null) as string | null; - const rawOrgs = (claims.organizations || claims.orgs || (claims.user_metadata as Record | undefined)?.organizations || (claims.app_metadata as Record | undefined)?.organizations) as DosOrgClaim[] | undefined; + const avatarUrl = ( + typeof claims.picture === 'string' + ? claims.picture + : typeof claims.avatar_url === 'string' + ? claims.avatar_url + : null + ) as string | null; + const rawOrgs = (claims.organizations || + claims.orgs || + (claims.user_metadata as Record | undefined)?.organizations || + (claims.app_metadata as Record | undefined)?.organizations) as DosOrgClaim[] | undefined; const organizations = Array.isArray(rawOrgs) ? rawOrgs : undefined; - const rawTeams = (claims.teams || (claims.user_metadata as Record | undefined)?.teams || (claims.app_metadata as Record | undefined)?.teams) as DosTeamClaim[] | undefined; + const rawTeams = (claims.teams || + (claims.user_metadata as Record | undefined)?.teams || + (claims.app_metadata as Record | undefined)?.teams) as DosTeamClaim[] | undefined; const teams = Array.isArray(rawTeams) ? rawTeams : undefined; - const activeOrgId = (typeof claims.active_org_id === 'string' ? claims.active_org_id : undefined) || (typeof claims.active_organization_id === 'string' ? claims.active_organization_id : undefined); + const activeOrgId = + (typeof claims.active_org_id === 'string' ? claims.active_org_id : undefined) || + (typeof claims.active_organization_id === 'string' ? claims.active_organization_id : undefined); if (typeof email !== 'string') { throw new AppError(AuthenticationErrorCode.InvalidRequest, { diff --git a/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts b/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts index 729a3928b4..cf45cb02bd 100644 --- a/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts +++ b/packages/auth/server/lib/utils/handle-oauth-organisation-callback-url.ts @@ -1,7 +1,7 @@ -import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/lib/server-only/organisation/sso/send-sso-link-confirmation-email'; import { formatPath } from '@documenso/lib/constants/app'; import { isDisposableEmail, isSignupEnabledForProvider } from '@documenso/lib/constants/auth'; import { AppError } from '@documenso/lib/errors/app-error'; +import { sendOrganisationAccountLinkConfirmationEmail } from '@documenso/lib/server-only/organisation/sso/send-sso-link-confirmation-email'; import { getEmailBlocklistDomains } from '@documenso/lib/server-only/site-settings/get-email-blocklist-domains'; import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user'; import { formatOrganisationLoginUrl } from '@documenso/lib/utils/organisation-authentication-portal'; diff --git a/packages/lib/jobs/client.ts b/packages/lib/jobs/client.ts index 0f302365d3..c9b2142ecb 100644 --- a/packages/lib/jobs/client.ts +++ b/packages/lib/jobs/client.ts @@ -26,9 +26,9 @@ import { CLEANUP_RATE_LIMITS_JOB_DEFINITION } from './definitions/internal/clean import { EXECUTE_WEBHOOK_JOB_DEFINITION } from './definitions/internal/execute-webhook'; import { EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION } from './definitions/internal/expire-recipients-sweep'; import { PROCESS_DOS_WEBHOOK_JOB_DEFINITION } from './definitions/internal/process-dos-webhook'; -import { PUBLISH_DOS_EVENT_JOB_DEFINITION } from './definitions/internal/publish-dos-event'; import { PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION } from './definitions/internal/process-recipient-expired'; import { PROCESS_SIGNING_REMINDER_JOB_DEFINITION } from './definitions/internal/process-signing-reminder'; +import { PUBLISH_DOS_EVENT_JOB_DEFINITION } from './definitions/internal/publish-dos-event'; import { RECONCILE_BLOCKCHAIN_ANCHORS_JOB_DEFINITION } from './definitions/internal/reconcile-blockchain-anchors'; import { SEAL_DOCUMENT_JOB_DEFINITION } from './definitions/internal/seal-document'; import { SEAL_DOCUMENT_SWEEP_JOB_DEFINITION } from './definitions/internal/seal-document-sweep'; diff --git a/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.handler.ts b/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.handler.ts index 79ad2bc341..6d6f26fce9 100644 --- a/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.handler.ts +++ b/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.handler.ts @@ -4,13 +4,7 @@ import { BlockchainAnchorStatus } from '@prisma/client'; import type { JobRunIO } from '../../client/_internal/job'; import type { TAnchorEnvelopeOnchainJobDefinition } from './anchor-envelope-onchain'; -export const run = async ({ - payload, - io, -}: { - payload: TAnchorEnvelopeOnchainJobDefinition; - io: JobRunIO; -}) => { +export const run = async ({ payload, io }: { payload: TAnchorEnvelopeOnchainJobDefinition; io: JobRunIO }) => { const { envelopeId, anchorKey } = payload; io.logger.info(`[Blockchain Anchor] Starting on-chain anchor job for envelope ${envelopeId}`); @@ -29,7 +23,9 @@ export const run = async ({ } if (anchor.status === BlockchainAnchorStatus.CONFIRMED) { - io.logger.info(`[Blockchain Anchor] Envelope ${envelopeId} already confirmed on-chain (UID: ${anchor.attestationUid})`); + io.logger.info( + `[Blockchain Anchor] Envelope ${envelopeId} already confirmed on-chain (UID: ${anchor.attestationUid})`, + ); return { success: true, attestationUid: anchor.attestationUid }; } @@ -71,9 +67,7 @@ export const run = async ({ await prisma.blockchainAnchor.update({ where: { id: anchor.id }, data: { - status: isRetryable - ? BlockchainAnchorStatus.RETRYABLE_FAILED - : BlockchainAnchorStatus.PERMANENT_FAILED, + status: isRetryable ? BlockchainAnchorStatus.RETRYABLE_FAILED : BlockchainAnchorStatus.PERMANENT_FAILED, lastError: errorMessage, }, }); diff --git a/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.ts b/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.ts index cd36ad2e15..ab0c215959 100644 --- a/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.ts +++ b/packages/lib/jobs/definitions/internal/anchor-envelope-onchain.ts @@ -9,9 +9,7 @@ const ANCHOR_ENVELOPE_ONCHAIN_JOB_DEFINITION_SCHEMA = z.object({ anchorKey: z.string().optional(), }); -export type TAnchorEnvelopeOnchainJobDefinition = z.infer< - typeof ANCHOR_ENVELOPE_ONCHAIN_JOB_DEFINITION_SCHEMA ->; +export type TAnchorEnvelopeOnchainJobDefinition = z.infer; export const ANCHOR_ENVELOPE_ONCHAIN_JOB_DEFINITION = { id: ANCHOR_ENVELOPE_ONCHAIN_JOB_DEFINITION_ID, diff --git a/packages/lib/jobs/definitions/internal/process-dos-webhook.handler.ts b/packages/lib/jobs/definitions/internal/process-dos-webhook.handler.ts index 543cae9fd1..f091a49262 100644 --- a/packages/lib/jobs/definitions/internal/process-dos-webhook.handler.ts +++ b/packages/lib/jobs/definitions/internal/process-dos-webhook.handler.ts @@ -2,13 +2,7 @@ import { handleDosWebhookEvent } from '../../../server-only/dos-id/handle-dos-we import type { JobRunIO } from '../../client/_internal/job'; import type { TProcessDosWebhookJobDefinition } from './process-dos-webhook'; -export const run = async ({ - payload, - io, -}: { - payload: TProcessDosWebhookJobDefinition; - io: JobRunIO; -}) => { +export const run = async ({ payload, io }: { payload: TProcessDosWebhookJobDefinition; io: JobRunIO }) => { io.logger.info(`[DOS Webhook Job] Processing event: ${payload.event} (id: ${payload.eventId || 'n/a'})`); const result = await handleDosWebhookEvent(payload); diff --git a/packages/lib/jobs/definitions/internal/process-dos-webhook.ts b/packages/lib/jobs/definitions/internal/process-dos-webhook.ts index 4b27bb6a2a..19c72dc76e 100644 --- a/packages/lib/jobs/definitions/internal/process-dos-webhook.ts +++ b/packages/lib/jobs/definitions/internal/process-dos-webhook.ts @@ -4,23 +4,25 @@ import type { JobDefinition } from '../../client/_internal/job'; const PROCESS_DOS_WEBHOOK_JOB_DEFINITION_ID = 'internal.process-dos-webhook'; -const PROCESS_DOS_WEBHOOK_JOB_DEFINITION_SCHEMA = z.object({ - eventId: z.string().optional(), - event: z.string(), - data: z.record(z.unknown()).optional(), - org_id: z.string().optional(), - name: z.string().optional(), - slug: z.string().optional(), - owner_id: z.string().optional(), - owner_email: z.string().optional(), - avatar_url: z.string().nullable().optional(), - org_name: z.string().optional(), - user_id: z.string().optional(), - user_email: z.string().optional(), - role: z.string().optional(), - email: z.string().optional(), - display_name: z.string().optional(), -}).passthrough(); +const PROCESS_DOS_WEBHOOK_JOB_DEFINITION_SCHEMA = z + .object({ + eventId: z.string().optional(), + event: z.string(), + data: z.record(z.unknown()).optional(), + org_id: z.string().optional(), + name: z.string().optional(), + slug: z.string().optional(), + owner_id: z.string().optional(), + owner_email: z.string().optional(), + avatar_url: z.string().nullable().optional(), + org_name: z.string().optional(), + user_id: z.string().optional(), + user_email: z.string().optional(), + role: z.string().optional(), + email: z.string().optional(), + display_name: z.string().optional(), + }) + .passthrough(); export type TProcessDosWebhookJobDefinition = z.infer; diff --git a/packages/lib/jobs/definitions/internal/publish-dos-event.handler.ts b/packages/lib/jobs/definitions/internal/publish-dos-event.handler.ts index 533200548c..c0948dd0f6 100644 --- a/packages/lib/jobs/definitions/internal/publish-dos-event.handler.ts +++ b/packages/lib/jobs/definitions/internal/publish-dos-event.handler.ts @@ -2,13 +2,7 @@ import { env } from '../../../utils/env'; import type { JobRunIO } from '../../client/_internal/job'; import type { TPublishDosEventJobDefinition } from './publish-dos-event'; -export const run = async ({ - payload, - io, -}: { - payload: TPublishDosEventJobDefinition; - io: JobRunIO; -}) => { +export const run = async ({ payload, io }: { payload: TPublishDosEventJobDefinition; io: JobRunIO }) => { const { event, data } = payload; const dosApiUrl = env('DOS_API_URL') || 'https://api.dos.me'; const apiKey = env('DOS_INTERNAL_API_KEY') || env('NEXT_PRIVATE_DOS_INTERNAL_API_KEY') || ''; @@ -37,9 +31,7 @@ export const run = async ({ if (!response.ok) { const errorText = await response.text().catch(() => ''); - io.logger.warn( - `[DOS Event Publisher] Failed with status ${response.status}: ${errorText}`, - ); + io.logger.warn(`[DOS Event Publisher] Failed with status ${response.status}: ${errorText}`); // Don't crash worker if external event router returns non-200 in dev if (response.status >= 500) { throw new Error(`[DOS Event Publisher] HTTP ${response.status}: ${errorText}`); diff --git a/packages/lib/jobs/definitions/internal/reconcile-blockchain-anchors.handler.ts b/packages/lib/jobs/definitions/internal/reconcile-blockchain-anchors.handler.ts index 2e4eee0a1e..8e573241e6 100644 --- a/packages/lib/jobs/definitions/internal/reconcile-blockchain-anchors.handler.ts +++ b/packages/lib/jobs/definitions/internal/reconcile-blockchain-anchors.handler.ts @@ -18,10 +18,7 @@ export const run = async ({ const stuckAnchors = await prisma.blockchainAnchor.findMany({ where: { status: { - in: [ - BlockchainAnchorStatus.PENDING, - BlockchainAnchorStatus.RETRYABLE_FAILED, - ], + in: [BlockchainAnchorStatus.PENDING, BlockchainAnchorStatus.RETRYABLE_FAILED], }, attempts: { lt: 5, diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts index a50c306596..6470c950d2 100644 --- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts +++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts @@ -15,6 +15,8 @@ import { groupBy } from 'remeda'; import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app'; import { AppError, AppErrorCode } from '../../../errors/app-error'; +import { computeMerkleRoot, hashBytes32, hashCanonicalJson } from '../../../server-only/blockchain/canonical-json'; +import { dispatchContractCompletedToCrm } from '../../../server-only/dos-id/crm-event-mapper'; import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf'; import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf'; import { insertFieldInPDFV1 } from '../../../server-only/pdf/insert-field-in-pdf-v1'; @@ -22,12 +24,6 @@ import { insertFieldInPDFV2 } from '../../../server-only/pdf/insert-field-in-pdf import { legacy_insertFieldInPDF } from '../../../server-only/pdf/legacy-insert-field-in-pdf'; import { getTeamSettings } from '../../../server-only/team/get-team-settings'; import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook'; -import { dispatchContractCompletedToCrm } from '../../../server-only/dos-id/crm-event-mapper'; -import { - computeMerkleRoot, - hashBytes32, - hashCanonicalJson, -} from '../../../server-only/blockchain/canonical-json'; import { DOCUMENT_AUDIT_LOG_TYPE, type TDocumentAuditLog } from '../../../types/document-audit-logs'; import { isTspEnvelope } from '../../../types/signature-level'; import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../../types/webhook-payload'; @@ -397,14 +393,16 @@ export const run = async ({ payload, io }: { payload: TSealDocumentJobDefinition // Trigger on-chain anchor worker asynchronously after successful commit if (!isRejected) { - void jobs.triggerJob({ - name: 'internal.anchor-envelope-onchain', - payload: { - envelopeId, - }, - }).catch((err) => { - console.warn(`[Blockchain Anchor] Failed to enqueue anchor job for envelope ${envelopeId}:`, err); - }); + void jobs + .triggerJob({ + name: 'internal.anchor-envelope-onchain', + payload: { + envelopeId, + }, + }) + .catch((err) => { + console.warn(`[Blockchain Anchor] Failed to enqueue anchor job for envelope ${envelopeId}:`, err); + }); } let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected; diff --git a/packages/lib/server-only/ai/google.test.ts b/packages/lib/server-only/ai/google.test.ts index 13dec1ede8..a1bdd69453 100644 --- a/packages/lib/server-only/ai/google.test.ts +++ b/packages/lib/server-only/ai/google.test.ts @@ -7,7 +7,8 @@ describe('parseGoogleServiceAccountKey', () => { const jsonKey = JSON.stringify({ type: 'service_account', project_id: 'my-gcp-project', - private_key: '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n', + private_key: + '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n', client_email: 'sa-crove-sign@my-gcp-project.iam.gserviceaccount.com', }); @@ -15,7 +16,8 @@ describe('parseGoogleServiceAccountKey', () => { expect(parsed).toEqual({ project_id: 'my-gcp-project', - private_key: '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n', + private_key: + '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...\n-----END PRIVATE KEY-----\n', client_email: 'sa-crove-sign@my-gcp-project.iam.gserviceaccount.com', }); }); diff --git a/packages/lib/server-only/ai/google.ts b/packages/lib/server-only/ai/google.ts index ae1f1b9150..79d9b977d2 100644 --- a/packages/lib/server-only/ai/google.ts +++ b/packages/lib/server-only/ai/google.ts @@ -8,25 +8,18 @@ export type GoogleServiceAccountCredentials = { project_id?: string; }; -export const parseGoogleServiceAccountKey = ( - rawKey?: string | null, -): GoogleServiceAccountCredentials | undefined => { +export const parseGoogleServiceAccountKey = (rawKey?: string | null): GoogleServiceAccountCredentials | undefined => { if (!rawKey) { return undefined; } try { const trimmed = rawKey.trim(); - const jsonStr = trimmed.startsWith('{') - ? trimmed - : Buffer.from(trimmed, 'base64').toString('utf8'); + const jsonStr = trimmed.startsWith('{') ? trimmed : Buffer.from(trimmed, 'base64').toString('utf8'); const parsed = JSON.parse(jsonStr) as Record; - if ( - typeof parsed.client_email !== 'string' || - typeof parsed.private_key !== 'string' - ) { + if (typeof parsed.client_email !== 'string' || typeof parsed.private_key !== 'string') { console.warn('GOOGLE_VERTEX_SERVICE_ACCOUNT_KEY is missing client_email or private_key'); return undefined; } diff --git a/packages/lib/server-only/blockchain/canonical-json.test.ts b/packages/lib/server-only/blockchain/canonical-json.test.ts index 23ddb69506..df9f98f0b1 100644 --- a/packages/lib/server-only/blockchain/canonical-json.test.ts +++ b/packages/lib/server-only/blockchain/canonical-json.test.ts @@ -1,11 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - canonicalizeJson, - computeMerkleRoot, - hashBytes32, - hashCanonicalJson, -} from './canonical-json'; +import { canonicalizeJson, computeMerkleRoot, hashBytes32, hashCanonicalJson } from './canonical-json'; describe('canonical-json (RFC 8785)', () => { it('should sort object keys deterministically', () => { diff --git a/packages/lib/server-only/blockchain/canonical-json.ts b/packages/lib/server-only/blockchain/canonical-json.ts index cc497a9583..6e9816acfe 100644 --- a/packages/lib/server-only/blockchain/canonical-json.ts +++ b/packages/lib/server-only/blockchain/canonical-json.ts @@ -69,17 +69,11 @@ export function computeMerkleRoot(leafHashes: string[]): string { for (let i = 0; i < currentLevel.length; i += 2) { if (i + 1 < currentLevel.length) { - const combined = Buffer.concat([ - Buffer.from(currentLevel[i], 'hex'), - Buffer.from(currentLevel[i + 1], 'hex'), - ]); + const combined = Buffer.concat([Buffer.from(currentLevel[i], 'hex'), Buffer.from(currentLevel[i + 1], 'hex')]); nextLevel.push(crypto.createHash('sha256').update(combined).digest('hex')); } else { // Odd node duplicated or carried up - const combined = Buffer.concat([ - Buffer.from(currentLevel[i], 'hex'), - Buffer.from(currentLevel[i], 'hex'), - ]); + const combined = Buffer.concat([Buffer.from(currentLevel[i], 'hex'), Buffer.from(currentLevel[i], 'hex')]); nextLevel.push(crypto.createHash('sha256').update(combined).digest('hex')); } } diff --git a/packages/lib/server-only/blockchain/outbox-worker.test.ts b/packages/lib/server-only/blockchain/outbox-worker.test.ts index 9f86c3e981..bd94a97c2a 100644 --- a/packages/lib/server-only/blockchain/outbox-worker.test.ts +++ b/packages/lib/server-only/blockchain/outbox-worker.test.ts @@ -1,13 +1,7 @@ import crypto from 'node:crypto'; import { describe, expect, it } from 'vitest'; -import { - canonicalizeJson, - computeMerkleRoot, - hashBytes32, - hashCanonicalJson, -} from './canonical-json'; -import { CROVE_EAS_SCHEMA_V2 } from './resolver-abi'; +import { computeMerkleRoot, hashBytes32, hashCanonicalJson } from './canonical-json'; describe('Blockchain Outbox & Attestation State Machine Simulation', () => { // Enum mirroring Prisma BlockchainAnchorStatus @@ -167,9 +161,7 @@ describe('Blockchain Outbox & Attestation State Machine Simulation', () => { anchorRecord.attempts += 1; anchorRecord.lastError = 'RPC Connection Timeout'; anchorRecord.status = - anchorRecord.attempts < 5 - ? BlockchainAnchorStatus.RETRYABLE_FAILED - : BlockchainAnchorStatus.PERMANENT_FAILED; + anchorRecord.attempts < 5 ? BlockchainAnchorStatus.RETRYABLE_FAILED : BlockchainAnchorStatus.PERMANENT_FAILED; expect(anchorRecord.status).toBe(BlockchainAnchorStatus.RETRYABLE_FAILED); expect(anchorRecord.attempts).toBe(1); @@ -177,8 +169,7 @@ describe('Blockchain Outbox & Attestation State Machine Simulation', () => { // Simulate Reconciliation Sweep picking up RETRYABLE_FAILED record const eligibleForReconcile = [anchorRecord].filter( (a) => - (a.status === BlockchainAnchorStatus.PENDING || - a.status === BlockchainAnchorStatus.RETRYABLE_FAILED) && + (a.status === BlockchainAnchorStatus.PENDING || a.status === BlockchainAnchorStatus.RETRYABLE_FAILED) && a.attempts < 5, ); expect(eligibleForReconcile.length).toBe(1); diff --git a/packages/lib/server-only/blockchain/resolver.test.ts b/packages/lib/server-only/blockchain/resolver.test.ts index 33e8e63f8c..f766479a61 100644 --- a/packages/lib/server-only/blockchain/resolver.test.ts +++ b/packages/lib/server-only/blockchain/resolver.test.ts @@ -1,11 +1,7 @@ import crypto from 'node:crypto'; import { describe, expect, it } from 'vitest'; -import { - CROVE_ANCHOR_GATEWAY_ABI, - CROVE_EAS_SCHEMA_V2, - CROVE_RESOLVER_ABI, -} from './resolver-abi'; +import { CROVE_ANCHOR_GATEWAY_ABI, CROVE_EAS_SCHEMA_V2, CROVE_RESOLVER_ABI } from './resolver-abi'; describe('CroveAttestationResolver ABI & Schema Contract', () => { it('should export the standard Crove Sign EAS schema v2', () => { @@ -15,9 +11,7 @@ describe('CroveAttestationResolver ABI & Schema Contract', () => { }); it('should include all required view functions for reverse lookup in the resolver ABI', () => { - const functionNames = CROVE_RESOLVER_ABI - .filter((item) => item.type === 'function') - .map((item) => item.name); + const functionNames = CROVE_RESOLVER_ABI.filter((item) => item.type === 'function').map((item) => item.name); expect(functionNames).toContain('getAttestationsByArtifactRoot'); expect(functionNames).toContain('getAttestationsByEnvelopeHash'); @@ -32,18 +26,14 @@ describe('CroveAttestationResolver ABI & Schema Contract', () => { }); it('should include the anchoring entrypoints and the indexing event in the gateway ABI', () => { - const functionNames = CROVE_ANCHOR_GATEWAY_ABI - .filter((item) => item.type === 'function') - .map((item) => item.name); + const functionNames = CROVE_ANCHOR_GATEWAY_ABI.filter((item) => item.type === 'function').map((item) => item.name); expect(functionNames).toContain('anchorEnvelope'); expect(functionNames).toContain('batchAnchorEnvelopes'); expect(functionNames).toContain('authorizedRelayers'); expect(functionNames).toContain('anchorKeyToUID'); - const eventNames = CROVE_ANCHOR_GATEWAY_ABI - .filter((item) => item.type === 'event') - .map((item) => item.name); + const eventNames = CROVE_ANCHOR_GATEWAY_ABI.filter((item) => item.type === 'event').map((item) => item.name); expect(eventNames).toContain('EnvelopeAnchored'); }); @@ -81,14 +71,10 @@ describe('CroveAttestationResolver ABI & Schema Contract', () => { const components = payloadInput.components ?? []; - expect(components.map((component) => component.name)).toEqual( - schemaFields.map((field) => field.name), - ); + expect(components.map((component) => component.name)).toEqual(schemaFields.map((field) => field.name)); // uint16/uint8 value types line up; bytes32 vs the schema's value types. - expect(components.map((component) => component.type)).toEqual( - schemaFields.map((field) => field.type), - ); + expect(components.map((component) => component.type)).toEqual(schemaFields.map((field) => field.type)); }); it('should correctly simulate reverse lookup mapping logic', () => { diff --git a/packages/lib/server-only/blockchain/verify-attestation.ts b/packages/lib/server-only/blockchain/verify-attestation.ts index 8f5a3c0901..ca4efce368 100644 --- a/packages/lib/server-only/blockchain/verify-attestation.ts +++ b/packages/lib/server-only/blockchain/verify-attestation.ts @@ -39,9 +39,7 @@ export type TBlockchainVerificationResult = { * evidence is dishonest. Re-introduce a fallback only once anchors carry a * real on-chain receipt. */ -export async function verifyDocumentFile( - pdfBuffer: Buffer | Uint8Array, -): Promise { +export async function verifyDocumentFile(pdfBuffer: Buffer | Uint8Array): Promise { const documentHash = hashBytes32(pdfBuffer); const anchor = await prisma.blockchainAnchor.findFirst({ @@ -107,9 +105,7 @@ export async function verifyDocumentFile( * `documentHash: '0x0'` when no anchor existed, so the public verification * portal confirmed documents that were never anchored on-chain. */ -export async function verifyDocumentByQrToken( - qrToken: string, -): Promise { +export async function verifyDocumentByQrToken(qrToken: string): Promise { const envelope = await prisma.envelope.findFirst({ where: { qrToken }, include: { diff --git a/packages/lib/server-only/document/is-recipient-authorized.otp-lockout.test.ts b/packages/lib/server-only/document/is-recipient-authorized.otp-lockout.test.ts index 09cfb28897..b9aadc7ca3 100644 --- a/packages/lib/server-only/document/is-recipient-authorized.otp-lockout.test.ts +++ b/packages/lib/server-only/document/is-recipient-authorized.otp-lockout.test.ts @@ -48,7 +48,9 @@ describe('validateEmailOtpWithLockout (M10/C3 recipient OTP lockout)', () => { expect(mocks.upsert).toHaveBeenCalledOnce(); - const upsertArg = mocks.upsert.mock.calls[0][0] as { where: { key_action_bucket: { key: string; action: string } } }; + const upsertArg = mocks.upsert.mock.calls[0][0] as { + where: { key_action_bucket: { key: string; action: string } }; + }; expect(upsertArg.where.key_action_bucket.key).toBe('otp-fail:env_1:signer@example.com'); expect(upsertArg.where.key_action_bucket.action).toBe('auth.recipient-email-otp'); }); diff --git a/packages/lib/server-only/document/is-recipient-authorized.ts b/packages/lib/server-only/document/is-recipient-authorized.ts index a094ae3c8b..dadaed4f7f 100644 --- a/packages/lib/server-only/document/is-recipient-authorized.ts +++ b/packages/lib/server-only/document/is-recipient-authorized.ts @@ -214,12 +214,9 @@ export const validateEmailOtpWithLockout = async ( if (failRecord && failRecord.count >= OTP_MAX_ATTEMPTS) { throw new AppError(AppErrorCode.TOO_MANY_REQUESTS, { - message: - 'Too many invalid verification attempts. Please request a new code and try again later.', + message: 'Too many invalid verification attempts. Please request a new code and try again later.', headers: { - 'Retry-After': String( - Math.max(1, Math.ceil((bucket.getTime() + OTP_LOCKOUT_WINDOW_MS - Date.now()) / 1000)), - ), + 'Retry-After': String(Math.max(1, Math.ceil((bucket.getTime() + OTP_LOCKOUT_WINDOW_MS - Date.now()) / 1000))), }, }); } diff --git a/packages/lib/server-only/dos-id/create-dos-organisation.ts b/packages/lib/server-only/dos-id/create-dos-organisation.ts index bcdb79228e..8928dca13f 100644 --- a/packages/lib/server-only/dos-id/create-dos-organisation.ts +++ b/packages/lib/server-only/dos-id/create-dos-organisation.ts @@ -22,11 +22,7 @@ export type CreateDosOrganisationOptions = { * * If API delegation is unavailable, it gracefully falls back to local creation. */ -export const createDosOrganisation = async ({ - userId, - name, - slug, -}: CreateDosOrganisationOptions) => { +export const createDosOrganisation = async ({ userId, name, slug }: CreateDosOrganisationOptions) => { const account = await prisma.account.findFirst({ where: { userId, diff --git a/packages/lib/server-only/dos-id/crm-event-mapper.ts b/packages/lib/server-only/dos-id/crm-event-mapper.ts index 63048376ad..6a63b58275 100644 --- a/packages/lib/server-only/dos-id/crm-event-mapper.ts +++ b/packages/lib/server-only/dos-id/crm-event-mapper.ts @@ -24,13 +24,9 @@ export type ContractCompletedEventData = { * Maps and dispatches a structured `contract.completed` / `contract.signed` event * directly formatted for ingestion by Twenty CRM and Crove Desk. */ -export async function dispatchContractCompletedToCrm( - eventData: ContractCompletedEventData, -): Promise { +export async function dispatchContractCompletedToCrm(eventData: ContractCompletedEventData): Promise { const baseUrl = NEXT_PUBLIC_WEBAPP_URL(); - const customerEmails = (eventData.recipients || []) - .map((r) => r.email.toLowerCase()) - .filter(Boolean); + const customerEmails = (eventData.recipients || []).map((r) => r.email.toLowerCase()).filter(Boolean); const payload: Record = { contract_id: eventData.envelopeId, diff --git a/packages/lib/server-only/dos-id/handle-dos-webhook.ts b/packages/lib/server-only/dos-id/handle-dos-webhook.ts index 457e868b95..3e5b64a3ab 100644 --- a/packages/lib/server-only/dos-id/handle-dos-webhook.ts +++ b/packages/lib/server-only/dos-id/handle-dos-webhook.ts @@ -33,7 +33,9 @@ export type DosWebhookPayload = { [key: string]: unknown; }; -export const handleDosWebhookEvent = async (payload: DosWebhookPayload): Promise<{ success: boolean; message: string }> => { +export const handleDosWebhookEvent = async ( + payload: DosWebhookPayload, +): Promise<{ success: boolean; message: string }> => { const event = (payload.event || '').toLowerCase(); const data = (payload.data && typeof payload.data === 'object' ? payload.data : payload) as Record; @@ -95,10 +97,7 @@ export const handleDosWebhookEvent = async (payload: DosWebhookPayload): Promise const org = await prisma.organisation.findFirst({ where: { - OR: [ - ...(orgId ? [{ id: orgId }] : []), - ...(slug ? [{ url: slug }] : []), - ], + OR: [...(orgId ? [{ id: orgId }] : []), ...(slug ? [{ url: slug }] : [])], }, }); @@ -124,10 +123,7 @@ export const handleDosWebhookEvent = async (payload: DosWebhookPayload): Promise const org = await prisma.organisation.findFirst({ where: { - OR: [ - ...(orgId ? [{ id: orgId }] : []), - ...(slug ? [{ url: slug }] : []), - ], + OR: [...(orgId ? [{ id: orgId }] : []), ...(slug ? [{ url: slug }] : [])], }, include: { teams: { select: { id: true } }, @@ -188,8 +184,7 @@ export const handleDosWebhookEvent = async (payload: DosWebhookPayload): Promise const orgRole = mapDosRoleToOrgRole(role); const targetGroup = org.groups.find( - (group) => - group.type === OrganisationGroupType.INTERNAL_ORGANISATION && group.organisationRole === orgRole, + (group) => group.type === OrganisationGroupType.INTERNAL_ORGANISATION && group.organisationRole === orgRole, ); const existingMember = await prisma.organisationMember.findUnique({ diff --git a/packages/lib/server-only/dos-id/sync-dos-profile.ts b/packages/lib/server-only/dos-id/sync-dos-profile.ts index e6e3fa9da8..d28d7dfbcb 100644 --- a/packages/lib/server-only/dos-id/sync-dos-profile.ts +++ b/packages/lib/server-only/dos-id/sync-dos-profile.ts @@ -142,13 +142,7 @@ export const mapDosRoleToTeamRole = (role?: string): TeamMemberRole => { /** * Provision or join an organization for a user given DOS ID claims. */ -export const syncOrganisationForUser = async ({ - userId, - org, -}: { - userId: number; - org: DosOrgClaim; -}) => { +export const syncOrganisationForUser = async ({ userId, org }: { userId: number; org: DosOrgClaim }) => { const rawOrgId = org.org_id || org.id; const orgName = org.name || org.slug || 'Organization'; const orgUrl = org.slug || rawOrgId || prefixedId('org'); @@ -173,8 +167,7 @@ export const syncOrganisationForUser = async ({ if (!isAlreadyMember) { const targetGroup = existingOrg.groups.find( - (group) => - group.type === OrganisationGroupType.INTERNAL_ORGANISATION && group.organisationRole === memberRole, + (group) => group.type === OrganisationGroupType.INTERNAL_ORGANISATION && group.organisationRole === memberRole, ); if (targetGroup) { @@ -249,9 +242,7 @@ export const syncOrganisationForUser = async ({ }, }); - const adminGroup = createdOrg.groups.find( - (group) => group.organisationRole === OrganisationMemberRole.ADMIN, - ); + const adminGroup = createdOrg.groups.find((group) => group.organisationRole === OrganisationMemberRole.ADMIN); if (adminGroup) { await tx.organisationMember.create({ @@ -379,9 +370,7 @@ export const syncTeamForUser = async ({ } // Find matching team group for the desired role - const matchingTeamGroup = existingTeam.teamGroups.find( - (tg) => tg.teamRole === teamRole, - ); + const matchingTeamGroup = existingTeam.teamGroups.find((tg) => tg.teamRole === teamRole); if (matchingTeamGroup) { const isAlreadyInGroup = matchingTeamGroup.organisationGroup.organisationGroupMembers.some( @@ -389,13 +378,15 @@ export const syncTeamForUser = async ({ ); if (!isAlreadyInGroup) { - await prisma.organisationGroupMember.create({ - data: { - id: generateDatabaseId('group_member'), - groupId: matchingTeamGroup.organisationGroupId, - organisationMemberId: orgMember.id, - }, - }).catch(() => null); + await prisma.organisationGroupMember + .create({ + data: { + id: generateDatabaseId('group_member'), + groupId: matchingTeamGroup.organisationGroupId, + organisationMemberId: orgMember.id, + }, + }) + .catch(() => null); } } }; @@ -523,10 +514,7 @@ export const syncDosProfileAndOrgs = async ({ team, organisationId: finalOrgId, }).catch((err) => { - console.error( - `[DOS ID] Failed to JIT sync team ${team.name || team.slug} for user ${userId}:`, - err, - ); + console.error(`[DOS ID] Failed to JIT sync team ${team.name || team.slug} for user ${userId}:`, err); }); } } diff --git a/packages/lib/server-only/dos-id/verify-dos-signature.ts b/packages/lib/server-only/dos-id/verify-dos-signature.ts index 1a3bf9e18d..6ead6c6c57 100644 --- a/packages/lib/server-only/dos-id/verify-dos-signature.ts +++ b/packages/lib/server-only/dos-id/verify-dos-signature.ts @@ -22,10 +22,7 @@ export const verifyDosWebhookSignature = ({ ? signatureHeader.slice(expectedPrefix.length) : signatureHeader; - const computedHash = crypto - .createHmac('sha256', secret) - .update(rawBody, 'utf8') - .digest('hex'); + const computedHash = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex'); if (providedHash.length !== computedHash.length) { return false; diff --git a/packages/lib/server-only/rate-limit/rate-limits.ts b/packages/lib/server-only/rate-limit/rate-limits.ts index 8ade612418..00a825c1f6 100644 --- a/packages/lib/server-only/rate-limit/rate-limits.ts +++ b/packages/lib/server-only/rate-limit/rate-limits.ts @@ -120,4 +120,3 @@ export const webhookIngestRateLimit = createRateLimit({ globalMax: 300, window: '1m', }); - diff --git a/packages/lib/universal/get-base-url.ts b/packages/lib/universal/get-base-url.ts index b516607a98..1a1b66d9e1 100644 --- a/packages/lib/universal/get-base-url.ts +++ b/packages/lib/universal/get-base-url.ts @@ -1,5 +1,5 @@ /* eslint-disable turbo/no-undeclared-env-vars */ -import { NEXT_PUBLIC_WEBAPP_URL, getBasePath } from '../constants/app'; +import { getBasePath, NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { env } from '../utils/env'; export const getBaseUrl = () => { diff --git a/packages/lib/utils/render-custom-email-template.test.ts b/packages/lib/utils/render-custom-email-template.test.ts index b1bd4d2c75..d790f60fd4 100644 --- a/packages/lib/utils/render-custom-email-template.test.ts +++ b/packages/lib/utils/render-custom-email-template.test.ts @@ -8,15 +8,15 @@ describe('renderCustomEmailTemplate', () => { }); it('replaces multiple variables separated by whitespace', () => { - expect( - renderCustomEmailTemplate('Hi {name}, sign at {url}', { name: 'Sam', url: 'https://x' }), - ).toBe('Hi Sam, sign at https://x'); + expect(renderCustomEmailTemplate('Hi {name}, sign at {url}', { name: 'Sam', url: 'https://x' })).toBe( + 'Hi Sam, sign at https://x', + ); }); it('replaces adjacent variables and variables separated by a non-whitespace character', () => { - expect( - renderCustomEmailTemplate('{day}/{month}/{year}', { day: '01', month: '02', year: '2026' }), - ).toBe('01/02/2026'); + expect(renderCustomEmailTemplate('{day}/{month}/{year}', { day: '01', month: '02', year: '2026' })).toBe( + '01/02/2026', + ); expect( renderCustomEmailTemplate('{firstName}-{lastName}', { diff --git a/scripts/benchmark-doschain-attestation.mjs b/scripts/benchmark-doschain-attestation.mjs index e73eaf54c6..48ba73679c 100644 --- a/scripts/benchmark-doschain-attestation.mjs +++ b/scripts/benchmark-doschain-attestation.mjs @@ -21,9 +21,15 @@ import { performance } from 'node:perf_hooks'; // ========================================================================= function canonicalizeJson(obj) { - if (obj === null || obj === undefined) return 'null'; - if (typeof obj === 'number' || typeof obj === 'boolean') return JSON.stringify(obj); - if (typeof obj === 'string') return JSON.stringify(obj); + if (obj === null || obj === undefined) { + return 'null'; + } + if (typeof obj === 'number' || typeof obj === 'boolean') { + return JSON.stringify(obj); + } + if (typeof obj === 'string') { + return JSON.stringify(obj); + } if (Array.isArray(obj)) { return `[${obj.map((item) => canonicalizeJson(item)).join(',')}]`; } @@ -46,23 +52,19 @@ function computeMerkleRoot(leafHashes) { if (leafHashes.length === 0) { return '0x0000000000000000000000000000000000000000000000000000000000000000'; } - if (leafHashes.length === 1) return leafHashes[0]; + if (leafHashes.length === 1) { + return leafHashes[0]; + } let currentLevel = leafHashes.map((h) => (h.startsWith('0x') ? h.slice(2) : h)); while (currentLevel.length > 1) { const nextLevel = []; for (let i = 0; i < currentLevel.length; i += 2) { if (i + 1 < currentLevel.length) { - const combined = Buffer.concat([ - Buffer.from(currentLevel[i], 'hex'), - Buffer.from(currentLevel[i + 1], 'hex'), - ]); + const combined = Buffer.concat([Buffer.from(currentLevel[i], 'hex'), Buffer.from(currentLevel[i + 1], 'hex')]); nextLevel.push(crypto.createHash('sha256').update(combined).digest('hex')); } else { - const combined = Buffer.concat([ - Buffer.from(currentLevel[i], 'hex'), - Buffer.from(currentLevel[i], 'hex'), - ]); + const combined = Buffer.concat([Buffer.from(currentLevel[i], 'hex'), Buffer.from(currentLevel[i], 'hex')]); nextLevel.push(crypto.createHash('sha256').update(combined).digest('hex')); } } @@ -115,8 +117,9 @@ async function runBenchmarks() { const testCounts = [1, 2, 5, 10, 25, 50]; for (const count of testCounts) { - const leafHashes = Array.from({ length: count }, (_, idx) => - `0x${crypto.createHash('sha256').update(`PDF Document Content ${idx}`).digest('hex')}` + const leafHashes = Array.from( + { length: count }, + (_, idx) => `0x${crypto.createHash('sha256').update(`PDF Document Content ${idx}`).digest('hex')}`, ); const startMerkle = performance.now(); @@ -134,13 +137,15 @@ async function runBenchmarks() { // Evidence Schema v2: 6 x bytes32 (192 bytes) + uint16 (32 bytes padded) + uint8 (32 bytes padded) = 256 bytes payload const singleAttestCalldataBytes = 4 + 32 + 32 + 32 + 32 + 32 + 32 + 256; // ~484 bytes const singleAttestGas = 21000 + singleAttestCalldataBytes * 16 + 45000; // Base + Calldata + State write (~65,000 - 85,000 gas) - + // Batch 5 items in multiAttest - const batch5Gas = 21000 + (singleAttestCalldataBytes * 5) * 16 + 45000 + 4 * 25000; // ~180,000 gas (average ~36k gas per document) - const gasSavingsPct = Math.round((1 - (batch5Gas / 5) / singleAttestGas) * 100); + const batch5Gas = 21000 + singleAttestCalldataBytes * 5 * 16 + 45000 + 4 * 25000; // ~180,000 gas (average ~36k gas per document) + const gasSavingsPct = Math.round((1 - batch5Gas / 5 / singleAttestGas) * 100); console.log(` ⛽ Single Document Attestation : ~${singleAttestGas.toLocaleString()} gas`); - console.log(` ⛽ Multi-Item Batch (5 items) : ~${batch5Gas.toLocaleString()} gas total (~${Math.round(batch5Gas / 5).toLocaleString()} gas/doc)`); + console.log( + ` ⛽ Multi-Item Batch (5 items) : ~${batch5Gas.toLocaleString()} gas total (~${Math.round(batch5Gas / 5).toLocaleString()} gas/doc)`, + ); console.log(` 💡 Multi-Attest Gas Efficiency : ${gasSavingsPct}% gas reduction per document\n`); // Test 4: DOS Chain RPC Network Check @@ -175,7 +180,9 @@ async function runBenchmarks() { } } catch (err) { console.log(` ℹ️ [Notice] RPC offline or unreachable from local machine (${err.message}).`); - console.log(` 🛡️ Crove Sign Outbox Worker handles network latency gracefully with retryable outbox persistence.\n`); + console.log( + ` 🛡️ Crove Sign Outbox Worker handles network latency gracefully with retryable outbox persistence.\n`, + ); } console.log('================================================================'); diff --git a/scripts/canary-testnet-attestation.mjs b/scripts/canary-testnet-attestation.mjs index d5fd707006..549a614aa2 100644 --- a/scripts/canary-testnet-attestation.mjs +++ b/scripts/canary-testnet-attestation.mjs @@ -23,7 +23,8 @@ const TESTNET_CONFIG = { rpcUrl: process.env.DOS_TESTNET_RPC || 'https://test.doschain.com', easAddress: '0x79799066b2b5072E4B154Bedde14Dbc22caa0EA5', schemaRegistry: '0x7979E91c465d3dde4faA7a6601b5b2c1C66c9999', - schemaV2: 'bytes32 envelopeHash, bytes32 artifactRoot, bytes32 auditBundleRoot, bytes32 identityEvidenceRoot, bytes32 riskEvidenceRoot, bytes32 policyHash, uint16 evidenceVersion, uint8 eventType', + schemaV2: + 'bytes32 envelopeHash, bytes32 artifactRoot, bytes32 auditBundleRoot, bytes32 identityEvidenceRoot, bytes32 riskEvidenceRoot, bytes32 policyHash, uint16 evidenceVersion, uint8 eventType', }; async function rpcCall(method, params = []) { @@ -72,7 +73,9 @@ async function main() { console.log(` ✅ RPC Online : Latency ${latencyMs} ms`); console.log(` 🧱 Current Block : #${blockNumber}\n`); } catch (err) { - console.warn(` ⚠️ RPC Probe Warning : ${err.message} (Outbox worker will queue and retry on network recovery)\n`); + console.warn( + ` ⚠️ RPC Probe Warning : ${err.message} (Outbox worker will queue and retry on network recovery)\n`, + ); } // Step 2: Check EAS Contract Bytecode @@ -82,7 +85,9 @@ async function main() { if (bytecode && bytecode !== '0x' && bytecode !== '0x0') { console.log(` ✅ EAS Bytecode Found : ${bytecode.slice(0, 34)}... (${Math.round(bytecode.length / 2)} bytes)`); } else { - console.log(` ℹ️ [Notice] Contract bytecode not deployed on this RPC instance. Schema will be registered when Testnet validator nodes complete restart.`); + console.log( + ` ℹ️ [Notice] Contract bytecode not deployed on this RPC instance. Schema will be registered when Testnet validator nodes complete restart.`, + ); } } catch (err) { console.warn(` ⚠️ Bytecode query note: ${err.message}`); @@ -94,8 +99,14 @@ async function main() { const mockEnvelopeId = `env_canary_${Date.now()}`; const mockPdfBytes = Buffer.from(`%PDF-1.7 Canary Document Contract - ${Date.now()}`); const artifactRoot = `0x${crypto.createHash('sha256').update(mockPdfBytes).digest('hex')}`; - const envelopeHash = `0x${crypto.createHash('sha256').update(JSON.stringify({ domain: 'CroveSign', id: mockEnvelopeId })).digest('hex')}`; - const auditBundleRoot = `0x${crypto.createHash('sha256').update(JSON.stringify({ event: 'DOCUMENT_COMPLETED', timestamp: new Date().toISOString() })).digest('hex')}`; + const envelopeHash = `0x${crypto + .createHash('sha256') + .update(JSON.stringify({ domain: 'CroveSign', id: mockEnvelopeId })) + .digest('hex')}`; + const auditBundleRoot = `0x${crypto + .createHash('sha256') + .update(JSON.stringify({ event: 'DOCUMENT_COMPLETED', timestamp: new Date().toISOString() })) + .digest('hex')}`; const anchorKey = `0x${crypto.createHash('sha256').update(`${mockEnvelopeId}:1:${artifactRoot}`).digest('hex')}`; console.log(` 📄 Mock Envelope ID : ${mockEnvelopeId}`); diff --git a/scripts/generate-vi-po.mjs b/scripts/generate-vi-po.mjs index e392be2119..9601121aa4 100644 --- a/scripts/generate-vi-po.mjs +++ b/scripts/generate-vi-po.mjs @@ -11,134 +11,143 @@ const viPoPath = path.join(ROOT_DIR, 'packages', 'lib', 'translations', 'vi', 'w // Dictionary of English to Vietnamese translations for Crove Sign / Documenso const VI_TRANSLATIONS = new Map([ - ["Default Document Language", "Ngôn ngữ tài liệu mặc định"], - ["Default document language", "Ngôn ngữ tài liệu mặc định"], - ["Default Document Visibility", "Quyền hiển thị tài liệu mặc định"], - ["Default document visibility", "Quyền hiển thị tài liệu mặc định"], - ["Default Date Format", "Định dạng ngày mặc định"], - ["Default date format", "Định dạng ngày mặc định"], - ["Default Email", "Email mặc định"], - ["Inherit from organisation", "Kế thừa từ tổ chức"], - ["Inherit from organization", "Kế thừa từ tổ chức"], - ["Vietnamese", "Tiếng Việt"], - ["English", "Tiếng Anh"], - ["French", "Tiếng Pháp"], - ["German", "Tiếng Đức"], - ["Spanish", "Tiếng Tây Ban Nha"], - ["Italian", "Tiếng Ý"], - ["Dutch", "Tiếng Hà Lan"], - ["Polish", "Tiếng Ba Lan"], - ["Portuguese (Brazil)", "Tiếng Bồ Đào Nha (Brazil)"], - ["Japanese", "Tiếng Nhật"], - ["Korean", "Tiếng Hàn"], - ["Chinese", "Tiếng Trung"], - ["Dashboard", "Bảng điều khiển"], - ["Documents", "Tài liệu"], - ["Templates", "Mẫu tài liệu"], - ["Signatures", "Chữ ký"], - ["Settings", "Cài đặt"], - ["Inbox", "Hộp thư"], - ["Profile", "Hồ sơ cá nhân"], - ["Team", "Đội nhóm"], - ["Teams", "Các đội nhóm"], - ["Organisation", "Tổ chức"], - ["Organisations", "Các tổ chức"], - ["Organization", "Tổ chức"], - ["Organizations", "Các tổ chức"], - ["General", "Chung"], - ["Members", "Thành viên"], - ["Billing", "Thanh toán & Gói dịch vụ"], - ["Public Profile", "Hồ sơ công khai"], - ["Security", "Bảo mật"], - ["Webhooks", "Webhooks"], - ["API Tokens", "Mã API Tokens"], - ["Email Domains", "Tên miền Email"], - ["Custom Branding", "Thương hiệu tùy chỉnh"], - ["Branding", "Thương hiệu"], - ["Audit Log", "Nhật ký kiểm toán"], - ["Audit Logs", "Nhật ký kiểm toán"], - ["Support", "Hỗ trợ"], - ["Help", "Trợ giúp"], - ["Sign In", "Đăng nhập"], - ["Sign Up", "Đăng ký"], - ["Sign Out", "Đăng xuất"], - ["Create Account", "Tạo tài khoản"], - ["Create Document", "Tạo tài liệu"], - ["Create Template", "Tạo mẫu tài liệu"], - ["Create Organisation", "Tạo tổ chức"], - ["Create Team", "Tạo đội nhóm"], - ["Create Webhook", "Tạo Webhook"], - ["Create Token", "Tạo Token"], - ["Save", "Lưu"], - ["Cancel", "Hủy"], - ["Delete", "Xóa"], - ["Edit", "Chỉnh sửa"], - ["Update", "Cập nhật"], - ["Continue", "Tiếp tục"], - ["Back", "Quay lại"], - ["Next", "Tiếp theo"], - ["Finish", "Hoàn thành"], - ["Sign", "Ký"], - ["Sign Document", "Ký tài liệu"], - ["Sign Document - Crove Sign", "Ký tài liệu - Crove Sign"], - ["Send", "Gửi"], - ["Send Document", "Gửi tài liệu"], - ["Resend", "Gửi lại"], - ["Download", "Tải xuống"], - ["Preview", "Xem trước"], - ["Copy Link", "Sao chép liên kết"], - ["Copied!", "Đã sao chép!"], - ["Search", "Tìm kiếm"], - ["Filter", "Lọc"], - ["Upload", "Tải lên"], - ["Upload Document", "Tải lên tài liệu"], - ["Confirm", "Xác nhận"], - ["Accept", "Chấp nhận"], - ["Decline", "Từ chối"], - ["Reject", "Từ chối"], - ["Duplicate", "Nhân bản"], - ["Draft", "Bản nháp"], - ["Pending", "Đang chờ ký"], - ["Completed", "Đã hoàn thành"], - ["Signed", "Đã ký"], - ["Rejected", "Đã từ chối"], - ["Cancelled", "Đã hủy"], - ["Expired", "Đã hết hạn"], - ["Full Name", "Họ và tên"], - ["Name", "Tên"], - ["Email", "Email"], - ["Email Address", "Địa chỉ Email"], - ["Password", "Mật khẩu"], - ["Current Password", "Mật khẩu hiện tại"], - ["New Password", "Mật khẩu mới"], - ["Confirm Password", "Xác nhận mật khẩu"], - ["Role", "Vai trò"], - ["Admin", "Quản trị viên"], - ["Manager", "Quản lý"], - ["Member", "Thành viên"], - ["Owner", "Chủ sở hữu"], - ["Signer", "Người ký"], - ["Approver", "Người phê duyệt"], - ["Viewer", "Người xem"], - ["Recipient", "Người nhận"], - ["Recipients", "Những người nhận"], - ["Signature", "Chữ ký"], - ["Initials", "Chữ ký tắt"], - ["Date", "Ngày tháng"], - ["Text", "Văn bản"], - ["Checkbox", "Hộp kiểm"], - ["Radio", "Nút chọn"], - ["Dropdown", "Menu chọn"], - ["Welcome to Crove Sign", "Chào mừng bạn đến với Crove Sign"], - ["Welcome to Crove Sign!", "Chào mừng bạn đến với Crove Sign!"], - ["Electronic Signature Disclosure", "Công bố về Chữ ký Điện tử"], - ["Your email has been successfully confirmed! You can now use all features of Crove Sign.", "Email của bạn đã được xác nhận thành công! Bạn có thể sử dụng đầy đủ các tính năng của Crove Sign."], - ["Your email has already been confirmed. You can now use all features of Crove Sign.", "Email của bạn đã được xác nhận trước đó. Bạn có thể sử dụng tất cả tính năng của Crove Sign."], - ["This document is available in your Crove Sign account. You can view more details, recipients, and audit logs there.", "Tài liệu này khả dụng trong tài khoản Crove Sign của bạn. Bạn có thể xem thêm chi tiết, người nhận và nhật ký kiểm toán tại đó."], - ["Use API tokens to authenticate with the Crove Sign API.", "Sử dụng API tokens để xác thực với Crove Sign API."], - ["The URL for Crove Sign to send webhook events to.", "URL endpoint để Crove Sign gửi sự kiện webhook đến."], - ["Read our documentation to get started with Crove Sign.", "Đọc tài liệu hướng dẫn để bắt đầu sử dụng Crove Sign."], - ["Return to Crove Sign sign in page here", "Quay lại trang đăng nhập Crove Sign tại đây"], + ['Default Document Language', 'Ngôn ngữ tài liệu mặc định'], + ['Default document language', 'Ngôn ngữ tài liệu mặc định'], + ['Default Document Visibility', 'Quyền hiển thị tài liệu mặc định'], + ['Default document visibility', 'Quyền hiển thị tài liệu mặc định'], + ['Default Date Format', 'Định dạng ngày mặc định'], + ['Default date format', 'Định dạng ngày mặc định'], + ['Default Email', 'Email mặc định'], + ['Inherit from organisation', 'Kế thừa từ tổ chức'], + ['Inherit from organization', 'Kế thừa từ tổ chức'], + ['Vietnamese', 'Tiếng Việt'], + ['English', 'Tiếng Anh'], + ['French', 'Tiếng Pháp'], + ['German', 'Tiếng Đức'], + ['Spanish', 'Tiếng Tây Ban Nha'], + ['Italian', 'Tiếng Ý'], + ['Dutch', 'Tiếng Hà Lan'], + ['Polish', 'Tiếng Ba Lan'], + ['Portuguese (Brazil)', 'Tiếng Bồ Đào Nha (Brazil)'], + ['Japanese', 'Tiếng Nhật'], + ['Korean', 'Tiếng Hàn'], + ['Chinese', 'Tiếng Trung'], + ['Dashboard', 'Bảng điều khiển'], + ['Documents', 'Tài liệu'], + ['Templates', 'Mẫu tài liệu'], + ['Signatures', 'Chữ ký'], + ['Settings', 'Cài đặt'], + ['Inbox', 'Hộp thư'], + ['Profile', 'Hồ sơ cá nhân'], + ['Team', 'Đội nhóm'], + ['Teams', 'Các đội nhóm'], + ['Organisation', 'Tổ chức'], + ['Organisations', 'Các tổ chức'], + ['Organization', 'Tổ chức'], + ['Organizations', 'Các tổ chức'], + ['General', 'Chung'], + ['Members', 'Thành viên'], + ['Billing', 'Thanh toán & Gói dịch vụ'], + ['Public Profile', 'Hồ sơ công khai'], + ['Security', 'Bảo mật'], + ['Webhooks', 'Webhooks'], + ['API Tokens', 'Mã API Tokens'], + ['Email Domains', 'Tên miền Email'], + ['Custom Branding', 'Thương hiệu tùy chỉnh'], + ['Branding', 'Thương hiệu'], + ['Audit Log', 'Nhật ký kiểm toán'], + ['Audit Logs', 'Nhật ký kiểm toán'], + ['Support', 'Hỗ trợ'], + ['Help', 'Trợ giúp'], + ['Sign In', 'Đăng nhập'], + ['Sign Up', 'Đăng ký'], + ['Sign Out', 'Đăng xuất'], + ['Create Account', 'Tạo tài khoản'], + ['Create Document', 'Tạo tài liệu'], + ['Create Template', 'Tạo mẫu tài liệu'], + ['Create Organisation', 'Tạo tổ chức'], + ['Create Team', 'Tạo đội nhóm'], + ['Create Webhook', 'Tạo Webhook'], + ['Create Token', 'Tạo Token'], + ['Save', 'Lưu'], + ['Cancel', 'Hủy'], + ['Delete', 'Xóa'], + ['Edit', 'Chỉnh sửa'], + ['Update', 'Cập nhật'], + ['Continue', 'Tiếp tục'], + ['Back', 'Quay lại'], + ['Next', 'Tiếp theo'], + ['Finish', 'Hoàn thành'], + ['Sign', 'Ký'], + ['Sign Document', 'Ký tài liệu'], + ['Sign Document - Crove Sign', 'Ký tài liệu - Crove Sign'], + ['Send', 'Gửi'], + ['Send Document', 'Gửi tài liệu'], + ['Resend', 'Gửi lại'], + ['Download', 'Tải xuống'], + ['Preview', 'Xem trước'], + ['Copy Link', 'Sao chép liên kết'], + ['Copied!', 'Đã sao chép!'], + ['Search', 'Tìm kiếm'], + ['Filter', 'Lọc'], + ['Upload', 'Tải lên'], + ['Upload Document', 'Tải lên tài liệu'], + ['Confirm', 'Xác nhận'], + ['Accept', 'Chấp nhận'], + ['Decline', 'Từ chối'], + ['Reject', 'Từ chối'], + ['Duplicate', 'Nhân bản'], + ['Draft', 'Bản nháp'], + ['Pending', 'Đang chờ ký'], + ['Completed', 'Đã hoàn thành'], + ['Signed', 'Đã ký'], + ['Rejected', 'Đã từ chối'], + ['Cancelled', 'Đã hủy'], + ['Expired', 'Đã hết hạn'], + ['Full Name', 'Họ và tên'], + ['Name', 'Tên'], + ['Email', 'Email'], + ['Email Address', 'Địa chỉ Email'], + ['Password', 'Mật khẩu'], + ['Current Password', 'Mật khẩu hiện tại'], + ['New Password', 'Mật khẩu mới'], + ['Confirm Password', 'Xác nhận mật khẩu'], + ['Role', 'Vai trò'], + ['Admin', 'Quản trị viên'], + ['Manager', 'Quản lý'], + ['Member', 'Thành viên'], + ['Owner', 'Chủ sở hữu'], + ['Signer', 'Người ký'], + ['Approver', 'Người phê duyệt'], + ['Viewer', 'Người xem'], + ['Recipient', 'Người nhận'], + ['Recipients', 'Những người nhận'], + ['Signature', 'Chữ ký'], + ['Initials', 'Chữ ký tắt'], + ['Date', 'Ngày tháng'], + ['Text', 'Văn bản'], + ['Checkbox', 'Hộp kiểm'], + ['Radio', 'Nút chọn'], + ['Dropdown', 'Menu chọn'], + ['Welcome to Crove Sign', 'Chào mừng bạn đến với Crove Sign'], + ['Welcome to Crove Sign!', 'Chào mừng bạn đến với Crove Sign!'], + ['Electronic Signature Disclosure', 'Công bố về Chữ ký Điện tử'], + [ + 'Your email has been successfully confirmed! You can now use all features of Crove Sign.', + 'Email của bạn đã được xác nhận thành công! Bạn có thể sử dụng đầy đủ các tính năng của Crove Sign.', + ], + [ + 'Your email has already been confirmed. You can now use all features of Crove Sign.', + 'Email của bạn đã được xác nhận trước đó. Bạn có thể sử dụng tất cả tính năng của Crove Sign.', + ], + [ + 'This document is available in your Crove Sign account. You can view more details, recipients, and audit logs there.', + 'Tài liệu này khả dụng trong tài khoản Crove Sign của bạn. Bạn có thể xem thêm chi tiết, người nhận và nhật ký kiểm toán tại đó.', + ], + ['Use API tokens to authenticate with the Crove Sign API.', 'Sử dụng API tokens để xác thực với Crove Sign API.'], + ['The URL for Crove Sign to send webhook events to.', 'URL endpoint để Crove Sign gửi sự kiện webhook đến.'], + ['Read our documentation to get started with Crove Sign.', 'Đọc tài liệu hướng dẫn để bắt đầu sử dụng Crove Sign.'], + ['Return to Crove Sign sign in page here', 'Quay lại trang đăng nhập Crove Sign tại đây'], ]); const enContent = fs.readFileSync(enPoPath, 'utf-8'); @@ -223,9 +232,7 @@ for (const entry of entries) { } // Extract raw string value of msgid - const rawMsgId = entry.msgid - .map((l) => JSON.parse(l)) - .join(''); + const rawMsgId = entry.msgid.map((l) => JSON.parse(l)).join(''); if (VI_TRANSLATIONS.has(rawMsgId)) { const translated = VI_TRANSLATIONS.get(rawMsgId);