diff --git a/.env.example b/.env.example index 05cee19c8f..0d462a6e1b 100644 --- a/.env.example +++ b/.env.example @@ -201,6 +201,11 @@ NEXT_PUBLIC_DISABLE_OIDC_SIGNIN= # to the OIDC provider (rendering only a spinner). Set to "true" to disable this # and keep showing the signin page. NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT= +# OPTIONAL: Break-glass password signin for redirect-only OIDC deployments. +# Comma-separated admin emails that may still sign in with a password via +# /signin?direct=1 while the OIDC provider is unreachable. Leave empty to +# disable the escape hatch entirely. +NEXT_PRIVATE_BREAK_GLASS_EMAILS= # OPTIONAL: Set to true to use internal webapp url in browserless requests. NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 2eb734c8d2..653340c8eb 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -5,7 +5,7 @@ const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, - async rewrites() { + rewrites() { return [ { source: '/docs/:path*.mdx', @@ -13,7 +13,7 @@ const config = { }, ]; }, - async redirects() { + redirects() { return [ // ============================================================ // Legacy docs site redirects (old site had no /docs prefix) diff --git a/apps/docs/src/app/docs/layout.tsx b/apps/docs/src/app/docs/layout.tsx index 39ee4ba602..ffbc38881d 100644 --- a/apps/docs/src/app/docs/layout.tsx +++ b/apps/docs/src/app/docs/layout.tsx @@ -35,13 +35,13 @@ const ROOT_SECTIONS = [ ]; // Find first page item in folder children -function getFirstPageUrl(children: PageTree.Node[]): string | undefined { +function _getFirstPageUrl(children: PageTree.Node[]): string | undefined { for (const child of children) { if (child.type === 'page') { return child.url; } if (child.type === 'folder' && child.children.length > 0) { - const url = getFirstPageUrl(child.children); + const url = _getFirstPageUrl(child.children); if (url) { return url; } diff --git a/apps/docs/src/app/og/docs/[...slug]/route.tsx b/apps/docs/src/app/og/docs/[...slug]/route.tsx index 76815dd3b6..3b9238b9cd 100644 --- a/apps/docs/src/app/og/docs/[...slug]/route.tsx +++ b/apps/docs/src/app/og/docs/[...slug]/route.tsx @@ -68,6 +68,7 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[... }} > {/* eslint-disable-next-line @next/next/no-img-element */} + {/* biome-ignore lint/performance/noImgElement: OG image route renders a raw logo bitmap outside the Next Image context */} Documenso { return null; } + // biome-ignore lint/security/noDangerouslySetInnerHtml: renders the SVG produced by mermaid.render() in the browser, not user-controlled HTML return
; }; diff --git a/apps/docs/src/mdx-components.tsx b/apps/docs/src/mdx-components.tsx index a0116880ae..64f689c000 100644 --- a/apps/docs/src/mdx-components.tsx +++ b/apps/docs/src/mdx-components.tsx @@ -4,7 +4,7 @@ import type { MDXComponents } from 'mdx/types'; import { EnvelopeWarning } from '@/components/mdx/envelope-warning'; import { Mermaid } from '@/components/mdx/mermaid'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// biome-ignore lint/suspicious/noExplicitAny: MDX component map typing mirrors upstream documenso export function getMDXComponents(components?: MDXComponents): any { return { ...defaultMdxComponents, diff --git a/apps/openpage-api/lib/cors.ts b/apps/openpage-api/lib/cors.ts index 78dcebde5a..1ec5b8642d 100644 --- a/apps/openpage-api/lib/cors.ts +++ b/apps/openpage-api/lib/cors.ts @@ -70,7 +70,7 @@ function getAllowedHeaders(req: Request, allowed?: string | string[]) { const headers = new Headers(); if (!allowed) { - allowed = req.headers.get('Access-Control-Request-Headers')!; + allowed = req.headers.get('Access-Control-Request-Headers') ?? undefined; headers.append('Vary', 'Access-Control-Request-Headers'); } else if (Array.isArray(allowed)) { allowed = allowed.join(','); diff --git a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts index 2e1eec0958..91c8ade0be 100644 --- a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts +++ b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts @@ -14,6 +14,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' .sum(fn.count('id')) // Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: Kysely window-function orderBy cannot be expressed type-safely (upstream workaround) .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']) as any)) .as('cume_count'), ]) diff --git a/apps/openpage-api/lib/growth/get-signer-conversion.ts b/apps/openpage-api/lib/growth/get-signer-conversion.ts index 464ae263ed..9cb18cc8d8 100644 --- a/apps/openpage-api/lib/growth/get-signer-conversion.ts +++ b/apps/openpage-api/lib/growth/get-signer-conversion.ts @@ -13,6 +13,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' = fn .sum(fn.count('Recipient.email').distinct()) // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: Kysely window-function orderBy cannot be expressed type-safely (upstream workaround) .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any)) .as('cume_count'), ]) diff --git a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts index 696266d694..631b526c37 100644 --- a/apps/openpage-api/lib/growth/get-user-monthly-growth.ts +++ b/apps/openpage-api/lib/growth/get-user-monthly-growth.ts @@ -12,6 +12,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count fn .sum(fn.count('id')) // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any + // biome-ignore lint/suspicious/noExplicitAny: Kysely window-function orderBy cannot be expressed type-safely (upstream workaround) .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'User.createdAt']) as any)) .as('cume_count'), ]) diff --git a/apps/openpage-api/lib/transform-data.ts b/apps/openpage-api/lib/transform-data.ts index cadeaac316..89ecb32b80 100644 --- a/apps/openpage-api/lib/transform-data.ts +++ b/apps/openpage-api/lib/transform-data.ts @@ -38,7 +38,7 @@ export function transformData({ data, metric }: { data: DataEntry; metric: Metri const [yearA, monthA] = dateA.split('-').map(Number); const [yearB, monthB] = dateB.split('-').map(Number); - if (isNaN(yearA) || isNaN(monthA) || isNaN(yearB) || isNaN(monthB)) { + if (Number.isNaN(yearA) || Number.isNaN(monthA) || Number.isNaN(yearB) || Number.isNaN(monthB)) { console.warn(`Invalid date format: ${dateA} or ${dateB}`); return 0; } @@ -54,7 +54,7 @@ export function transformData({ data, metric }: { data: DataEntry; metric: Metri try { const [year, month] = date.split('-'); - if (!year || !month || isNaN(Number(year)) || isNaN(Number(month))) { + if (!year || !month || Number.isNaN(Number(year)) || Number.isNaN(Number(month))) { console.warn(`Invalid date format: ${date}`); return date; } @@ -83,14 +83,14 @@ export function transformData({ data, metric }: { data: DataEntry; metric: Metri label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: sortedEntries.map(([_, stats]) => { const value = stats[metric]; - return typeof value === 'number' && !isNaN(value) ? value : 0; + return typeof value === 'number' && !Number.isNaN(value) ? value : 0; }), }, ], }; return addZeroMonth(transformedData, true); - } catch (error) { + } catch { return { labels: [], datasets: [{ label: `Total ${FRIENDLY_METRIC_NAMES[metric]}`, data: [] }], diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 6270bc948c..29f6fd5de7 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -65,6 +65,12 @@ export type SignInFormProps = { isOIDCSSOEnabled?: boolean; oidcProviderLabel?: string; returnTo?: string; + /** + * Hide when the password form is only reachable via the break-glass door: + * the forgot-password flow stays disabled suite-wide in that mode, so the + * link would dead-end. + */ + showForgotPasswordLink?: boolean; }; export const SignInForm = ({ @@ -76,6 +82,7 @@ export const SignInForm = ({ isOIDCSSOEnabled, oidcProviderLabel, returnTo, + showForgotPasswordLink = true, }: SignInFormProps) => { const { _ } = useLingui(); const { toast } = useToast(); @@ -362,14 +369,16 @@ export const SignInForm = ({ -

- - Forgot your password? - -

+ {showForgotPasswordLink && ( +

+ + Forgot your password? + +

+ )} )} /> diff --git a/apps/remix/app/routes/_authenticated+/_layout.tsx b/apps/remix/app/routes/_authenticated+/_layout.tsx index 9c1054b237..762b1f8942 100644 --- a/apps/remix/app/routes/_authenticated+/_layout.tsx +++ b/apps/remix/app/routes/_authenticated+/_layout.tsx @@ -4,6 +4,7 @@ import { OrganisationProvider } from '@documenso/lib/client-only/providers/organ import { useSession } from '@documenso/lib/client-only/providers/session'; import { getSiteSettings } from '@documenso/lib/server-only/site-settings/get-site-settings'; import { SITE_SETTINGS_BANNER_ID } from '@documenso/lib/server-only/site-settings/schemas/banner'; +import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; import { msg } from '@lingui/core/macro'; @@ -34,6 +35,15 @@ export async function loader({ request }: Route.LoaderArgs) { ]); if (!session.isAuthenticated) { + // Preserve the originally requested path (including query string) so the + // OIDC login round-trip lands back on the page the user wanted. + const requestUrl = new URL(request.url); + const returnTo = `${requestUrl.pathname}${requestUrl.search}`; + + if (isValidReturnTo(returnTo)) { + throw redirect(`/signin?returnTo=${encodeURIComponent(normalizeReturnTo(returnTo) ?? returnTo)}`); + } + throw redirect('/signin'); } diff --git a/apps/remix/app/routes/_unauthenticated+/signin.tsx b/apps/remix/app/routes/_unauthenticated+/signin.tsx index 4f9920fc3b..3b13832b4b 100644 --- a/apps/remix/app/routes/_unauthenticated+/signin.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signin.tsx @@ -5,6 +5,7 @@ import { IS_MICROSOFT_SSO_ENABLED, IS_OIDC_AUTO_REDIRECT_DISABLED, IS_OIDC_SSO_ENABLED, + isBreakGlassSigninEnabled, isSigninEnabledForProvider, isSignupEnabledForProvider, OIDC_PROVIDER_LABEL, @@ -44,6 +45,10 @@ export async function loader({ request }: Route.LoaderArgs) { const shouldAutoRedirectToOIDC = isOIDCOnlyTransport && !IS_OIDC_AUTO_REDIRECT_DISABLED; + // Break-glass escape hatch: when password signin is disabled suite-wide, + // allowlisted admins can still reach the password form via ?direct=1. + const isBreakGlassAvailable = isBreakGlassSigninEnabled(); + const oidcProviderLabel = OIDC_PROVIDER_LABEL; const isSignupEnabled = @@ -66,6 +71,7 @@ export async function loader({ request }: Route.LoaderArgs) { isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, + isBreakGlassAvailable, oidcProviderLabel, returnTo, shouldAutoRedirectToOIDC, @@ -79,6 +85,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, + isBreakGlassAvailable, oidcProviderLabel, returnTo, shouldAutoRedirectToOIDC, @@ -92,6 +99,16 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { const errorParam = searchParams.get('error'); const signupError = errorParam ? SIGNUP_ERROR_MESSAGES[errorParam] : undefined; + // Suppress the automatic IdP redirect when the user has explicitly asked + // for the break-glass form, when the IdP bounced us back with an error + // (otherwise we would restart the OIDC dance in a loop), or when embedded + // in a signing widget. + const isBreakGlassRequested = searchParams.get('direct') === '1' && isBreakGlassAvailable; + const hasIdpError = errorParam !== null; + + const shouldRedirectToOIDC = + shouldAutoRedirectToOIDC && !isBreakGlassRequested && !hasIdpError && !isEmbeddedRedirect; + useEffect(() => { const hash = window.location.hash.slice(1); @@ -101,14 +118,20 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { }, []); useEffect(() => { - if (!shouldAutoRedirectToOIDC) { + if (!shouldRedirectToOIDC) { + return; + } + + // Guard against the initial render racing the embedded detection above: + // read the hash synchronously so embedded contexts never bounce to the IdP. + if (new URLSearchParams(window.location.hash.slice(1)).get('embedded') === 'true') { return; } void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); - }, [shouldAutoRedirectToOIDC, returnTo]); + }, [shouldRedirectToOIDC, returnTo]); - if (shouldAutoRedirectToOIDC) { + if (shouldRedirectToOIDC) { return (
@@ -140,7 +163,8 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
{ + if (!shouldRedirectToOIDC) { + return; + } + + void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); + }, [shouldRedirectToOIDC, returnTo]); + + if (shouldRedirectToOIDC) { + return ( +
+
+ +

+ Redirecting to {oidcProviderLabel || 'OIDC'}... +

+
+
+ ); + } + return ( Browser Crove Sign (App) DOS.Me ID (Supabase A │<── Redirect to /inbox ────────┤ │ ``` +### 4.3. Redirect-Only Mode & Break-Glass + +When email/password signin and signup are disabled (`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN/SIGNUP=true`) and OIDC is the only enabled transport, `/signin` and `/signup` skip the button page entirely and auto-redirect to the OIDC provider (opt out with `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true`). The automatic redirect is suppressed when: + +- The IdP bounced the user back with `?error=...` (prevents a redirect loop; the error alert and a manual retry button are shown instead). +- The URL carries `#embedded=true` (embedded signing widgets must not bounce to the IdP). +- The URL carries `?direct=1` and the break-glass allowlist is configured. + +**Break-glass** (`NEXT_PRIVATE_BREAK_GLASS_EMAILS`, comma-separated admin emails): with password signin disabled suite-wide, allowlisted admins keep a manual password escape hatch via `/signin?direct=1` for use when the OIDC provider is unreachable. The allowlist is enforced server-side in `POST /api/auth/email-password/authorize` (non-allowlisted emails still receive `SigninDisabled`), so the credential-stuffing surface stays limited to the admin emails. Regular users have no password path. + +Security notes: the `SigninDisabled` vs `InvalidCredentials` distinction means a prober can infer allowlist membership; this is accepted as rate-limited, low-signal (admin emails are rarely secret). The break-glass also overrides the suite-wide `NEXT_PUBLIC_DISABLE_SIGNIN` master switch for allowlisted admins by design, so a login lockout incident still leaves admins a recovery path. The forgot-password flow stays disabled in this mode; the break-glass form hides its "Forgot your password?" link to avoid a dead end. + +Deep links are preserved end to end: unauthenticated access to authenticated routes redirects to `/signin?returnTo=`, and `returnTo` is validated (`isValidReturnTo`) and carried through the OIDC round-trip back to the original page. + --- ## 5. Crove OS 2-Tier Hybrid Architecture & Data Sync diff --git a/packages/app-tests/e2e/user/password.spec.ts b/packages/app-tests/e2e/user/password.spec.ts index 6bee822624..f09a811752 100644 --- a/packages/app-tests/e2e/user/password.spec.ts +++ b/packages/app-tests/e2e/user/password.spec.ts @@ -153,7 +153,7 @@ test('[USER] password reset invalidates all sessions', async ({ page }: { page: await page.context().addCookies(initialCookies); await page.goto('http://localhost:3000/settings/profile'); - await expect(page).toHaveURL('http://localhost:3000/signin'); + await expect(page).toHaveURL(/^http:\/\/localhost:3000\/signin/); expect(await checkSessionValid(page)).toBe(false); @@ -209,7 +209,7 @@ test('[USER] password update invalidates other sessions but keeps current', asyn await page.context().clearCookies(); await page.context().addCookies(initialCookies); await page.goto('http://localhost:3000/settings/profile'); - await expect(page).toHaveURL('http://localhost:3000/signin'); + await expect(page).toHaveURL(/^http:\/\/localhost:3000\/signin/); expect(await checkSessionValid(page)).toBe(false); await page.context().clearCookies(); diff --git a/packages/auth/server/routes/email-password.ts b/packages/auth/server/routes/email-password.ts index 8d890b81a6..6cdda13847 100644 --- a/packages/auth/server/routes/email-password.ts +++ b/packages/auth/server/routes/email-password.ts @@ -1,4 +1,5 @@ import { + isBreakGlassEmail, isDisposableEmail, isEmailDomainAllowedForSignup, isSigninEnabledForProvider, @@ -65,14 +66,17 @@ export const emailPasswordRoute = new Hono() .post('/authorize', sValidator('json', ZSignInSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); - if (!isSigninEnabledForProvider('email')) { + const { email, password, totpCode, backupCode, csrfToken, captchaToken } = c.req.valid('json'); + + // Break-glass: when password signin is disabled suite-wide, allowlisted + // admin emails (see NEXT_PRIVATE_BREAK_GLASS_EMAILS) may still sign in + // via /signin?direct=1 while the OIDC provider is unreachable. + if (!isSigninEnabledForProvider('email') && !isBreakGlassEmail(email)) { throw new AppError(AuthenticationErrorCode.SigninDisabled, { statusCode: 400, }); } - const { email, password, totpCode, backupCode, csrfToken, captchaToken } = c.req.valid('json'); - const loginLimitResult = await loginRateLimit.check({ ip: requestMetadata.ipAddress ?? 'unknown', identifier: email, diff --git a/packages/lib/constants/auth.test.ts b/packages/lib/constants/auth.test.ts new file mode 100644 index 0000000000..06403ab1e8 --- /dev/null +++ b/packages/lib/constants/auth.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { getBreakGlassEmails, isBreakGlassEmail, isBreakGlassSigninEnabled } from './auth'; + +describe('break-glass password signin allowlist', () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('returns an empty allowlist when the env var is unset or blank', () => { + expect(getBreakGlassEmails()).toEqual([]); + expect(isBreakGlassSigninEnabled()).toBe(false); + + vi.stubEnv('NEXT_PRIVATE_BREAK_GLASS_EMAILS', ''); + + expect(getBreakGlassEmails()).toEqual([]); + expect(isBreakGlassSigninEnabled()).toBe(false); + }); + + it('parses comma-separated emails with surrounding whitespace and mixed case', () => { + vi.stubEnv('NEXT_PRIVATE_BREAK_GLASS_EMAILS', ' Joy@Dos.AI , admin@crove.com ,, '); + + expect(getBreakGlassEmails()).toEqual(['joy@dos.ai', 'admin@crove.com']); + expect(isBreakGlassSigninEnabled()).toBe(true); + }); + + it('matches allowlisted emails case-insensitively and rejects everyone else', () => { + vi.stubEnv('NEXT_PRIVATE_BREAK_GLASS_EMAILS', 'Joy@dos.ai'); + + expect(isBreakGlassEmail('joy@dos.ai')).toBe(true); + expect(isBreakGlassEmail(' JOY@DOS.AI ')).toBe(true); + expect(isBreakGlassEmail('admin@dos.ai')).toBe(false); + expect(isBreakGlassEmail('')).toBe(false); + }); +}); diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index cb514979b5..20230a0e5f 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -34,6 +34,31 @@ export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL'); */ export const IS_OIDC_AUTO_REDIRECT_DISABLED = env('NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT') === 'true'; +/** + * Break-glass password signin allowlist for redirect-only OIDC deployments. + * + * When email/password signin is disabled suite-wide (redirect-only DOS ID + * login), these comma-separated admin emails keep a manual password escape + * hatch reachable via `/signin?direct=1`, for use when the OIDC provider is + * unreachable. Regular users have no password path. + */ +export const getBreakGlassEmails = (): string[] => { + const emails = env('NEXT_PRIVATE_BREAK_GLASS_EMAILS'); + + if (!emails) { + return []; + } + + return emails + .split(',') + .map((email) => email.trim().toLowerCase()) + .filter(Boolean); +}; + +export const isBreakGlassSigninEnabled = () => getBreakGlassEmails().length > 0; + +export const isBreakGlassEmail = (email: string) => getBreakGlassEmails().includes(email.trim().toLowerCase()); + export const USER_SECURITY_AUDIT_LOG_MAP: Record = { ACCOUNT_SSO_LINK: 'Linked account to SSO', ACCOUNT_SSO_UNLINK: 'Unlinked account from SSO', @@ -131,7 +156,7 @@ export const isEmailDomainAllowedForSignup = (email: string): boolean => { * pre-normalised (trimmed + lowercased) by the caller. * * Returns `true` when the email is disposable and should be rejected. - * Email format validation is intentionally NOT performed here — that is + * Email format validation is intentionally NOT performed here; that is * handled by Zod upstream. */ export const isDisposableEmail = (email: string, additionalBlockedDomains: string[] = []): boolean => { diff --git a/scripts/count-entries.mjs b/scripts/count-entries.mjs index ee05855b5e..3d6495702f 100644 --- a/scripts/count-entries.mjs +++ b/scripts/count-entries.mjs @@ -12,7 +12,9 @@ const content = fs.readFileSync(enPoPath, 'utf-8'); const lines = content.split('\n'); let count = 0; for (const line of lines) { - if (line.startsWith('msgid ')) count++; + if (line.startsWith('msgid ')) { + count++; + } } console.log('Total msgid count:', count); diff --git a/scripts/deploy-crove-resolver.mjs b/scripts/deploy-crove-resolver.mjs index 176ff4ea75..6c123fd289 100644 --- a/scripts/deploy-crove-resolver.mjs +++ b/scripts/deploy-crove-resolver.mjs @@ -10,13 +10,12 @@ * node scripts/deploy-crove-resolver.mjs [--network=dos-testnet|dos-mainnet] [--dry-run] */ -import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const ROOT_DIR = path.resolve(__dirname, '..'); +const _ROOT_DIR = path.resolve(__dirname, '..'); // ========================================== // 1. NETWORK CONFIGURATION & CONSTANTS @@ -46,7 +45,7 @@ const NETWORKS = { const CROVE_SCHEMA_V2 = 'bytes32 envelopeHash, bytes32 artifactRoot, bytes32 auditBundleRoot, bytes32 identityEvidenceRoot, bytes32 riskEvidenceRoot, bytes32 policyHash, uint16 evidenceVersion, uint8 eventType'; -async function main() { +function main() { console.log('\n====================================================================='); console.log('🚀 Crove Sign - EAS Gateway & Resolver Deployment Guide (DOS Chain)'); console.log('=====================================================================\n'); @@ -64,8 +63,12 @@ async function main() { console.log(`📜 Schema v2 Definition : "${CROVE_SCHEMA_V2}"\n`); console.log('📋 Contract Architecture & Deliverables:'); - console.log(' - CroveAnchorGateway : contracts/gateway/CroveAnchorGateway.sol (Anti-replay, Relayer Access Control, Multi-PDF batching)'); - console.log(' - CroveResolver : contracts/resolver/CroveResolver.sol (Reverse lookup: artifactRoot -> UID[], envelopeHash -> UID[])'); + console.log( + ' - CroveAnchorGateway : contracts/gateway/CroveAnchorGateway.sol (Anti-replay, Relayer Access Control, Multi-PDF batching)', + ); + console.log( + ' - CroveResolver : contracts/resolver/CroveResolver.sol (Reverse lookup: artifactRoot -> UID[], envelopeHash -> UID[])', + ); console.log(' - EAS Base Resolver : contracts/resolver/SchemaResolver.sol'); console.log(' - EAS Interfaces : contracts/interfaces/IEAS.sol, ISchemaResolver.sol'); console.log(' - TypeScript ABI : packages/lib/server-only/blockchain/resolver-abi.ts\n'); @@ -76,18 +79,24 @@ async function main() { console.log(' $env:DEPLOYER_PRIVATE_KEY=""; node scripts/deploy-crove-resolver.mjs --network=dos-testnet\n'); console.log('📌 Ordered Deployment Pipeline:'); console.log(' Step 1: Deploy CroveResolver(easAddress, ownerAddress, address(0))'); - console.log(' Step 2: Register Schema v2 on SchemaRegistry.register(schema, croveResolverAddress, revocable=false)'); + console.log( + ' Step 2: Register Schema v2 on SchemaRegistry.register(schema, croveResolverAddress, revocable=false)', + ); console.log(' Step 3: Deploy CroveAnchorGateway(easAddress, ownerAddress, croveRelayerAddress, schemaUID)'); console.log(' Step 4: Configure CroveResolver.setTrustedGateway(croveAnchorGatewayAddress)'); console.log(' Step 5: Configure CroveResolver.setSchemaUID(schemaUID)'); - console.log(' Step 6: Update CROVE_ANCHOR_GATEWAY_ADDRESS and CROVE_RESOLVER_ADDRESS in .env & docs/ARCHITECTURE.md\n'); + console.log( + ' Step 6: Update CROVE_ANCHOR_GATEWAY_ADDRESS and CROVE_RESOLVER_ADDRESS in .env & docs/ARCHITECTURE.md\n', + ); return; } console.log('⏳ Connecting to RPC and executing deployment...'); } -main().catch((err) => { +try { + main(); +} catch (err) { console.error('❌ Deployment error:', err); process.exit(1); -}); +} diff --git a/scripts/generate-comprehensive-vi-po.mjs b/scripts/generate-comprehensive-vi-po.mjs index de7c435d85..f3c6bb8bb5 100644 --- a/scripts/generate-comprehensive-vi-po.mjs +++ b/scripts/generate-comprehensive-vi-po.mjs @@ -12,256 +12,277 @@ const viPoPath = path.join(ROOT_DIR, 'packages', 'lib', 'translations', 'vi', 'w // Comprehensive dictionary for Documenso / Crove Sign e-signing platform const VI_TRANSLATIONS = new Map([ // Navigation & Shell - ["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ư"], - ["Personal Inbox", "Hộp thư cá nhân"], - ["Profile", "Hồ sơ cá nhân"], - ["Account", "Tài khoả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"], - ["Language", "Ngôn ngữ"], - ["Preferences", "Tùy chọn"], - ["Reminders", "Nhắc nhở ký"], - ["Certificates", "Chứng chỉ & Chứng thư"], - ["Groups", "Nhóm quyền"], - ["ORGANISATION SETTINGS", "CÀI ĐẶT TỔ CHỨC"], - ["TEAM SETTINGS", "CÀI ĐẶT ĐỘI NHÓM"], - ["ACCOUNT SETTINGS", "CÀI ĐẶT TÀI KHOẢN"], + ['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ư'], + ['Personal Inbox', 'Hộp thư cá nhân'], + ['Profile', 'Hồ sơ cá nhân'], + ['Account', 'Tài khoả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'], + ['Language', 'Ngôn ngữ'], + ['Preferences', 'Tùy chọn'], + ['Reminders', 'Nhắc nhở ký'], + ['Certificates', 'Chứng chỉ & Chứng thư'], + ['Groups', 'Nhóm quyền'], + ['ORGANISATION SETTINGS', 'CÀI ĐẶT TỔ CHỨC'], + ['TEAM SETTINGS', 'CÀI ĐẶT ĐỘI NHÓM'], + ['ACCOUNT SETTINGS', 'CÀI ĐẶT TÀI KHOẢN'], // Document Preferences & Settings - ["Document Preferences", "Tùy chọn tài liệu"], - ["Document preferences", "Tùy chọn tài liệu"], - ["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 Time Zone", "Múi giờ mặc định"], - ["Default time zone", "Múi giờ mặc định"], - ["Local timezone", "Múi giờ địa phương"], - ["Default Signature Settings", "Cài đặt chữ ký mặc định"], - ["Default signature settings", "Cài đặt chữ ký mặc định"], - ["Default Recipients", "Người nhận mặc định"], - ["Default recipients", "Người nhận mặc định"], - ["Default Email", "Email mặc định"], - ["Default Email Settings", "Cài đặt Email mặc định"], - ["Default Envelope Expiration", "Thời hạn gói tài liệu mặc định"], - ["Default file", "Tệp mặc định"], - ["Inherit from organisation", "Kế thừa từ tổ chức"], - ["Inherit from organization", "Kế thừa từ tổ chức"], - ["Everyone can access and view the document", "Mọi người đều có thể truy cập và xem tài liệu"], - ["Only recipients and owner can access", "Chỉ người nhận và chủ sở hữu mới có thể truy cập"], - ["Controls the default visibility of an uploaded document.", "Kiểm soát quyền hiển thị mặc định của tài liệu được tải lên."], - ["Controls the default language of an uploaded document. This will be used as the language in email communications with the recipients.", "Kiểm soát ngôn ngữ mặc định của tài liệu được tải lên. Ngôn ngữ này sẽ được sử dụng trong các email gửi tới người nhận."], - ["Controls which signatures are allowed to be used when signing a document.", "Kiểm soát các loại chữ ký được phép sử dụng khi ký tài liệu."], - ["Recipients that will be automatically added to new documents.", "Những người nhận sẽ được tự động thêm vào các tài liệu mới."], - ["Type, Draw, Upload", "Nhập chữ, Vẽ, Tải ảnh lên"], - ["Type", "Nhập chữ"], - ["Draw", "Vẽ chữ ký"], - ["Select or enter email address", "Chọn hoặc nhập địa chỉ email"], - ["Select language", "Chọn ngôn ngữ"], - ["Select timezone", "Chọn múi giờ"], - ["Select date format", "Chọn định dạng ngày"], - ["Select...", "Chọn..."], - ["Search...", "Tìm kiếm..."], - ["Search languages...", "Tìm kiếm ngôn ngữ..."], - ["Search documents", "Tìm kiếm tài liệu"], - ["Search templates", "Tìm kiếm mẫu tài liệu"], - ["Search members", "Tìm kiếm thành viên"], - ["Search webhooks", "Tìm kiếm webhooks"], - ["Search tokens", "Tìm kiếm mã tokens"], + ['Document Preferences', 'Tùy chọn tài liệu'], + ['Document preferences', 'Tùy chọn tài liệu'], + ['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 Time Zone', 'Múi giờ mặc định'], + ['Default time zone', 'Múi giờ mặc định'], + ['Local timezone', 'Múi giờ địa phương'], + ['Default Signature Settings', 'Cài đặt chữ ký mặc định'], + ['Default signature settings', 'Cài đặt chữ ký mặc định'], + ['Default Recipients', 'Người nhận mặc định'], + ['Default recipients', 'Người nhận mặc định'], + ['Default Email', 'Email mặc định'], + ['Default Email Settings', 'Cài đặt Email mặc định'], + ['Default Envelope Expiration', 'Thời hạn gói tài liệu mặc định'], + ['Default file', 'Tệp mặc định'], + ['Inherit from organisation', 'Kế thừa từ tổ chức'], + ['Inherit from organization', 'Kế thừa từ tổ chức'], + ['Everyone can access and view the document', 'Mọi người đều có thể truy cập và xem tài liệu'], + ['Only recipients and owner can access', 'Chỉ người nhận và chủ sở hữu mới có thể truy cập'], + [ + 'Controls the default visibility of an uploaded document.', + 'Kiểm soát quyền hiển thị mặc định của tài liệu được tải lên.', + ], + [ + 'Controls the default language of an uploaded document. This will be used as the language in email communications with the recipients.', + 'Kiểm soát ngôn ngữ mặc định của tài liệu được tải lên. Ngôn ngữ này sẽ được sử dụng trong các email gửi tới người nhận.', + ], + [ + 'Controls which signatures are allowed to be used when signing a document.', + 'Kiểm soát các loại chữ ký được phép sử dụng khi ký tài liệu.', + ], + [ + 'Recipients that will be automatically added to new documents.', + 'Những người nhận sẽ được tự động thêm vào các tài liệu mới.', + ], + ['Type, Draw, Upload', 'Nhập chữ, Vẽ, Tải ảnh lên'], + ['Type', 'Nhập chữ'], + ['Draw', 'Vẽ chữ ký'], + ['Select or enter email address', 'Chọn hoặc nhập địa chỉ email'], + ['Select language', 'Chọn ngôn ngữ'], + ['Select timezone', 'Chọn múi giờ'], + ['Select date format', 'Chọn định dạng ngày'], + ['Select...', 'Chọn...'], + ['Search...', 'Tìm kiếm...'], + ['Search languages...', 'Tìm kiếm ngôn ngữ...'], + ['Search documents', 'Tìm kiếm tài liệu'], + ['Search templates', 'Tìm kiếm mẫu tài liệu'], + ['Search members', 'Tìm kiếm thành viên'], + ['Search webhooks', 'Tìm kiếm webhooks'], + ['Search tokens', 'Tìm kiếm mã tokens'], // Languages - ["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"], + ['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'], // Actions & Buttons - ["Sign In", "Đăng nhập"], - ["Sign Up", "Đăng ký"], - ["Sign Out", "Đăng xuất"], - ["Log In", "Đăng nhập"], - ["Log 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 Organization", "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"], - ["Save Changes", "Lưu thay đổi"], - ["Save changes", "Lưu thay đổi"], - ["Cancel", "Hủy"], - ["Delete", "Xóa"], - ["Delete Document", "Xóa tài liệu"], - ["Delete Template", "Xóa mẫu tài liệu"], - ["Edit", "Chỉnh sửa"], - ["Update", "Cập nhật"], - ["Continue", "Tiếp tục"], - ["Back", "Quay lại"], - ["Back to home", "Quay lại trang chủ"], - ["Next", "Tiếp theo"], - ["Finish", "Hoàn thành"], - ["Sign", "Ký"], - ["Sign Document", "Ký tài liệu"], - ["Sign document", "Ký tài liệu"], - ["Sign Document - Crove Sign", "Ký tài liệu - Crove Sign"], - ["Sign Document - Documenso", "Ký tài liệu - Crove Sign"], - ["Sign Now", "Ký ngay"], - ["Send", "Gửi"], - ["Send Document", "Gửi tài liệu"], - ["Send document", "Gửi tài liệu"], - ["Resend", "Gửi lại"], - ["Download", "Tải xuống"], - ["Download Document", "Tải xuống tài liệu"], - ["Download Certificate", "Tải xuống chứng chỉ ký"], - ["Preview", "Xem trước"], - ["Preview Document", "Xem trước tài liệu"], - ["Copy Link", "Sao chép liên kết"], - ["Copied!", "Đã sao chép!"], - ["Filter", "Lọc"], - ["Upload", "Tải lên"], - ["Upload Document", "Tải lên tài liệu"], - ["Upload PDF", "Tải lên tệp PDF"], - ["Confirm", "Xác nhận"], - ["Accept", "Chấp nhận"], - ["Decline", "Từ chối"], - ["Reject", "Từ chối"], - ["Duplicate", "Nhân bản"], - ["Rename", "Đổi tên"], - ["Add Recipient", "Thêm người nhận"], - ["Add Field", "Thêm trường ký"], - ["Add Field...", "Thêm trường..."], - ["Add Team", "Thêm đội nhóm"], - ["Add Member", "Thêm thành viên"], - ["Invite Member", "Mời thành viên"], - ["Invite Members", "Mời các thành viên"], - ["Manage Members", "Quản lý thành viên"], - ["Manage Teams", "Quản lý đội nhóm"], - ["Leave Team", "Rời khỏi đội nhóm"], - ["Leave Organisation", "Rời khỏi tổ chức"], + ['Sign In', 'Đăng nhập'], + ['Sign Up', 'Đăng ký'], + ['Sign Out', 'Đăng xuất'], + ['Log In', 'Đăng nhập'], + ['Log 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 Organization', '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'], + ['Save Changes', 'Lưu thay đổi'], + ['Save changes', 'Lưu thay đổi'], + ['Cancel', 'Hủy'], + ['Delete', 'Xóa'], + ['Delete Document', 'Xóa tài liệu'], + ['Delete Template', 'Xóa mẫu tài liệu'], + ['Edit', 'Chỉnh sửa'], + ['Update', 'Cập nhật'], + ['Continue', 'Tiếp tục'], + ['Back', 'Quay lại'], + ['Back to home', 'Quay lại trang chủ'], + ['Next', 'Tiếp theo'], + ['Finish', 'Hoàn thành'], + ['Sign', 'Ký'], + ['Sign Document', 'Ký tài liệu'], + ['Sign document', 'Ký tài liệu'], + ['Sign Document - Crove Sign', 'Ký tài liệu - Crove Sign'], + ['Sign Document - Documenso', 'Ký tài liệu - Crove Sign'], + ['Sign Now', 'Ký ngay'], + ['Send', 'Gửi'], + ['Send Document', 'Gửi tài liệu'], + ['Send document', 'Gửi tài liệu'], + ['Resend', 'Gửi lại'], + ['Download', 'Tải xuống'], + ['Download Document', 'Tải xuống tài liệu'], + ['Download Certificate', 'Tải xuống chứng chỉ ký'], + ['Preview', 'Xem trước'], + ['Preview Document', 'Xem trước tài liệu'], + ['Copy Link', 'Sao chép liên kết'], + ['Copied!', 'Đã sao chép!'], + ['Filter', 'Lọc'], + ['Upload', 'Tải lên'], + ['Upload Document', 'Tải lên tài liệu'], + ['Upload PDF', 'Tải lên tệp PDF'], + ['Confirm', 'Xác nhận'], + ['Accept', 'Chấp nhận'], + ['Decline', 'Từ chối'], + ['Reject', 'Từ chối'], + ['Duplicate', 'Nhân bản'], + ['Rename', 'Đổi tên'], + ['Add Recipient', 'Thêm người nhận'], + ['Add Field', 'Thêm trường ký'], + ['Add Field...', 'Thêm trường...'], + ['Add Team', 'Thêm đội nhóm'], + ['Add Member', 'Thêm thành viên'], + ['Invite Member', 'Mời thành viên'], + ['Invite Members', 'Mời các thành viên'], + ['Manage Members', 'Quản lý thành viên'], + ['Manage Teams', 'Quản lý đội nhóm'], + ['Leave Team', 'Rời khỏi đội nhóm'], + ['Leave Organisation', 'Rời khỏi tổ chức'], // Document Signing View & Fields - ["Adopt and Sign", "Chấp nhận và Ký"], - ["Adopt Signature", "Chấp nhận chữ ký"], - ["Draw Signature", "Vẽ chữ ký"], - ["Type Signature", "Nhập chữ ký"], - ["Upload Signature", "Tải lên ảnh chữ ký"], - ["Clear", "Xóa vẽ lại"], - ["Clear signature", "Xóa chữ ký vẽ lại"], - ["Your Signature", "Chữ ký của bạn"], - ["Your Initials", "Chữ ký tắt của bạn"], - ["Required", "Bắt buộc"], - ["Optional", "Tùy chọn"], - ["Read Only", "Chỉ đọc"], - ["Read only", "Chỉ đọc"], - ["Signature Field", "Trường chữ ký"], - ["Initials Field", "Trường chữ ký tắt"], - ["Date Field", "Trường ngày tháng"], - ["Text Field", "Trường văn bản"], - ["Number Field", "Trường số"], - ["Checkbox Field", "Trường hộp kiểm"], - ["Radio Field", "Trường nút chọn"], - ["Dropdown Field", "Trường danh sách chọn"], - ["Insert Signature", "Chèn chữ ký"], - ["Insert Initials", "Chèn chữ ký tắt"], - ["Insert Date", "Chèn ngày tháng"], - ["Insert Text", "Chèn văn bản"], - ["Click to sign", "Nhấn để ký"], - ["Click to add initials", "Nhấn để thêm chữ ký tắt"], - ["Click to enter date", "Nhấn để nhập ngày"], - ["Click to enter text", "Nhấn để nhập văn bản"], + ['Adopt and Sign', 'Chấp nhận và Ký'], + ['Adopt Signature', 'Chấp nhận chữ ký'], + ['Draw Signature', 'Vẽ chữ ký'], + ['Type Signature', 'Nhập chữ ký'], + ['Upload Signature', 'Tải lên ảnh chữ ký'], + ['Clear', 'Xóa vẽ lại'], + ['Clear signature', 'Xóa chữ ký vẽ lại'], + ['Your Signature', 'Chữ ký của bạn'], + ['Your Initials', 'Chữ ký tắt của bạn'], + ['Required', 'Bắt buộc'], + ['Optional', 'Tùy chọn'], + ['Read Only', 'Chỉ đọc'], + ['Read only', 'Chỉ đọc'], + ['Signature Field', 'Trường chữ ký'], + ['Initials Field', 'Trường chữ ký tắt'], + ['Date Field', 'Trường ngày tháng'], + ['Text Field', 'Trường văn bản'], + ['Number Field', 'Trường số'], + ['Checkbox Field', 'Trường hộp kiểm'], + ['Radio Field', 'Trường nút chọn'], + ['Dropdown Field', 'Trường danh sách chọn'], + ['Insert Signature', 'Chèn chữ ký'], + ['Insert Initials', 'Chèn chữ ký tắt'], + ['Insert Date', 'Chèn ngày tháng'], + ['Insert Text', 'Chèn văn bản'], + ['Click to sign', 'Nhấn để ký'], + ['Click to add initials', 'Nhấn để thêm chữ ký tắt'], + ['Click to enter date', 'Nhấn để nhập ngày'], + ['Click to enter text', 'Nhấn để nhập văn bản'], // Document & Recipient Statuses - ["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"], - ["Inbox empty", "Hộp thư trống"], - ["No documents found", "Không tìm thấy tài liệu nào"], - ["No templates found", "Không tìm thấy mẫu nào"], + ['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'], + ['Inbox empty', 'Hộp thư trống'], + ['No documents found', 'Không tìm thấy tài liệu nào'], + ['No templates found', 'Không tìm thấy mẫu nào'], // Form Fields & Roles - ["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"], - ["Number", "Số"], - ["Radio", "Nút chọn một"], - ["Checkbox", "Hộp kiểm"], - ["Dropdown", "Danh sách chọ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'], + ['Number', 'Số'], + ['Radio', 'Nút chọn một'], + ['Checkbox', 'Hộp kiểm'], + ['Dropdown', 'Danh sách chọn'], // Brand / Crove Ecosystem strings - ["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!"], - ["Welcome to Documenso", "Chào mừng bạn đến với Crove Sign"], - ["Welcome to Documenso!", "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"], - ["An error occurred. Please try again.", "Đã xảy ra lỗi. Vui lòng thử lại."], - ["Something went wrong.", "Đã có sự cố xảy ra."], - ["All rights reserved.", "Đã đăng ký bản quyề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!'], + ['Welcome to Documenso', 'Chào mừng bạn đến với Crove Sign'], + ['Welcome to Documenso!', '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'], + ['An error occurred. Please try again.', 'Đã xảy ra lỗi. Vui lòng thử lại.'], + ['Something went wrong.', 'Đã có sự cố xảy ra.'], + ['All rights reserved.', 'Đã đăng ký bản quyền.'], ]); // Word replacement map @@ -303,7 +324,9 @@ const PHRASE_REPLACEMENTS = [ ]; function translateString(englishStr) { - if (!englishStr || englishStr.trim() === '') return ''; + if (!englishStr || englishStr.trim() === '') { + return ''; + } // 1. Exact dictionary match if (VI_TRANSLATIONS.has(englishStr)) { @@ -406,9 +429,7 @@ for (const entry of entries) { continue; } - const rawMsgId = entry.msgid - .map((l) => JSON.parse(l)) - .join(''); + const rawMsgId = entry.msgid.map((l) => JSON.parse(l)).join(''); const translatedText = translateString(rawMsgId); diff --git a/scripts/patch-crove-branding.mjs b/scripts/patch-crove-branding.mjs index 71f76123a2..eb3cd23b59 100644 --- a/scripts/patch-crove-branding.mjs +++ b/scripts/patch-crove-branding.mjs @@ -87,7 +87,9 @@ function patchTranslationCatalogs() { for (const locale of localeDirs) { const poFilePath = path.join(translationsDir, locale, 'web.po'); - if (!fs.existsSync(poFilePath)) continue; + if (!fs.existsSync(poFilePath)) { + continue; + } let content = fs.readFileSync(poFilePath, 'utf-8'); const originalContent = content; @@ -159,9 +161,11 @@ function patchWebManifests() { for (const manifestPath of manifestPaths) { const dir = path.dirname(manifestPath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } - fs.writeFileSync(manifestPath, JSON.stringify(manifestData, null, 2) + '\n', 'utf-8'); + fs.writeFileSync(manifestPath, `${JSON.stringify(manifestData, null, 2)}\n`, 'utf-8'); console.log(` ✓ Updated manifest: ${manifestPath}`); } console.log(' ✅ Finished updating PWA manifests.\n'); @@ -249,7 +253,15 @@ export const BrandingLogo = ({ className = 'h-6 w-auto', ...props }: LogoProps) console.log(` ✓ Created/updated logo component: ${brandingLogoPath}`); // 3. Branding Logo Icon Component - const brandingIconPath = path.join(ROOT_DIR, 'apps', 'remix', 'app', 'components', 'general', 'branding-logo-icon.tsx'); + const brandingIconPath = path.join( + ROOT_DIR, + 'apps', + 'remix', + 'app', + 'components', + 'general', + 'branding-logo-icon.tsx', + ); const brandingIconContent = `import type { SVGAttributes } from 'react'; export type LogoProps = SVGAttributes; diff --git a/scripts/sync-upstream.mjs b/scripts/sync-upstream.mjs index 38ac0830f2..c05606dd9f 100644 --- a/scripts/sync-upstream.mjs +++ b/scripts/sync-upstream.mjs @@ -68,7 +68,9 @@ async function getLatestUpstreamReleaseTag() { } function ensureUpstreamRemote() { - const remotes = runCmdOutput('git remote').split('\n').map((r) => r.trim()); + const remotes = runCmdOutput('git remote') + .split('\n') + .map((r) => r.trim()); if (!remotes.includes('upstream')) { console.log(`🔗 Adding upstream remote: ${UPSTREAM_REPO_URL}`); runCmd(`git remote add upstream ${UPSTREAM_REPO_URL}`); @@ -112,8 +114,10 @@ async function main() { console.log(`\n🔀 Merging upstream release ${targetTag} into ${currentBranch}...`); try { runCmd(`git merge ${targetTag} --no-edit -m "chore(sync): merge upstream Documenso release ${targetTag}"`); - } catch (err) { - console.error(`\n❌ Conflict encountered while merging ${targetTag}. Please resolve conflicts, run 'npm run patch:branding', and commit.`); + } catch (_err) { + console.error( + `\n❌ Conflict encountered while merging ${targetTag}. Please resolve conflicts, run 'npm run patch:branding', and commit.`, + ); process.exit(1); }