From e0ef11e8c3a42fdecc99337e52f4c89c1bc9735f Mon Sep 17 00:00:00 2001 From: Konrad <11725227+mKoonrad@users.noreply.github.com> Date: Thu, 25 Jun 2026 05:24:40 +0200 Subject: [PATCH 01/41] chore(i18n): update Polish translation (#3020) --- packages/lib/translations/pl/web.po | 511 ++++++++++++++-------------- 1 file changed, 256 insertions(+), 255 deletions(-) diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index 5fbfa727c..010ea2157 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -8,7 +8,7 @@ msgstr "" "Language: pl\n" "Project-Id-Version: documenso-app\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2026-06-22 13:53\n" +"PO-Revision-Date: 2026-06-23 13:05\n" "Last-Translator: \n" "Language-Team: Polish\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" @@ -31,7 +31,7 @@ msgstr "Adres „{0}” nie jest prawidłowym adresem e-mail." #. placeholder {1}: timezone || '' #: apps/remix/app/components/general/document-signing/document-signing-date-field.tsx msgid "\"{0}\" will appear on the document as it has a timezone of \"{1}\"." -msgstr "W dokumencie, z racji strefy czasowej „{1}”, pojawi się wartość „{0}”." +msgstr "Ze względu na strefę czasową „{1}”, w dokumencie pojawi się wartość „{0}”." #: packages/email/template-components/template-document-super-delete.tsx msgid "\"{documentName}\" has been deleted by an admin." @@ -51,7 +51,7 @@ msgstr "Użytkownik „{placeholderEmail}” z zespołu „Zespół X” zaprosi #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx msgid "\"{title}\" has been successfully cancelled" -msgstr "Dokument „{title}” został pomyślnie anulowany" +msgstr "Dokument „{title}” został anulowany" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "\"{title}\" has been successfully deleted" @@ -69,7 +69,7 @@ msgstr "„Zespół X” zaprosił Cię do podpisania dokumentu „ABC”." #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "(line {0})" -msgstr "(wiersz {0})" +msgstr "(linia {0})" #: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx #: apps/remix/app/components/general/document-signing/document-signing-form.tsx @@ -104,13 +104,13 @@ msgstr "{0, plural, one {Pozostał # znak} few {Pozostały # znaki} many {Pozost #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "{0, plural, one {# CSS rule was dropped during sanitisation.} other {# CSS rules were dropped during sanitisation.}}" -msgstr "{0, plural, one {# reguła CSS została odrzucona podczas oczyszczania.} few {# reguły CSS zostały odrzucone podczas oczyszczania.} many {# reguł CSS zostało odrzuconych podczas oczyszczania.} other {# reguły CSS zostały odrzucone podczas oczyszczania.}}" +msgstr "{0, plural, one {# reguła CSS została usunięta podczas oczyszczenia kodu.} few {# reguły CSS zostały usunięte podczas oczyszczenia kodu.} many {# reguł CSS zostało usuniętych podczas oczyszczenia kodu.} other {# reguły CSS zostały usunięte podczas oczyszczenia kodu.}}" #. placeholder {0}: result.cancelledCount #. placeholder {1}: result.failedIds.length #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "{0, plural, one {# document cancelled.} other {# documents cancelled.}} {1, plural, one {# document could not be cancelled.} other {# documents could not be cancelled.}}" -msgstr "{0, plural, one {Anulowano # dokument.} few {Anulowano # dokumenty.} many {Anulowano # dokumentów.} other {Anulowano # dokumentów.}} {1, plural, one {Nie udało się anulować # dokumentu.} few {Nie udało się anulować # dokumentów.} many {Nie udało się anulować # dokumentów.} other {Nie udało się anulować # dokumentów.}}" +msgstr "{0, plural, one {Anulowano # dokument.} few {Anulowano # dokumenty.} many {Anulowano # dokumentów.} other {Anulowano # dokumentów.}} {1, plural, one {Nie można było anulować # dokumentu.} few {Nie można było anulować # dokumentów.} many {Nie można było anulować # dokumentów.} other {Nie można było anulować # dokumentów.}}" #. placeholder {0}: result.cancelledCount #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx @@ -184,12 +184,12 @@ msgstr "{0, plural, one {<0>Masz <1>1 oczekujące zaproszenie} few {<2>M #: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx #: apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx msgid "{0, plural, one {1 Field Remaining} other {# Fields Remaining}}" -msgstr "{0, plural, one {Pozostało 1 pole} few {Pozostały # pola} many {Pozostało # pól} other {Pozostało # pola}}" +msgstr "{0, plural, one {Pozostało 1 pole} few {Pozostały # pola} many {Pozostało # pól} other {Pozostało # pól}}" #. placeholder {0}: fieldsOnExcessPages.length #: apps/remix/app/components/dialogs/envelope-item-edit-dialog.tsx msgid "{0, plural, one {1 field will be deleted because the new PDF has fewer pages than the current one.} other {# fields will be deleted because the new PDF has fewer pages than the current one.}}" -msgstr "{0, plural, one {1 pole zostanie usunięte, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} few {# pola zostaną usunięte, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} many {# pól zostanie usuniętych, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} other {# pola zostanie usuniętych, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.}}" +msgstr "{0, plural, one {1 pole zostanie usunięte, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} few {# pola zostaną usunięte, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} many {# pól zostanie usuniętych, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.} other {# pól zostanie usuniętych, ponieważ nowy plik PDF ma mniejszą liczbę stron niż obecny.}}" #. placeholder {0}: fields.filter((field) => field.envelopeItemId === doc.id).length #. placeholder {0}: remainingFields.filter((field) => field.envelopeItemId === doc.id).length @@ -264,7 +264,7 @@ msgstr "{0, plural, one {Znaleźliśmy # odbiorcę w dokumencie.} few {Znaleźli #. placeholder {0}: envelopeIds.length #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "{0, plural, one {You are about to cancel the selected document.} other {You are about to cancel # documents.}}" -msgstr "{0, plural, one {Zamierzasz anulować zaznaczony dokument.} few {Zamierzasz anulować # dokumenty.} many {Zamierzasz anulować # dokumentów.} other {Zamierzasz anulować # dokumentów.}}" +msgstr "{0, plural, one {Zamierzasz anulować dokument.} few {Zamierzasz anulować # dokumenty.} many {Zamierzasz anulować # dokumentów.} other {Zamierzasz anulować # dokumentów.}}" #. placeholder {0}: envelopeIds.length #: apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx @@ -318,7 +318,7 @@ msgstr "Pozostało {0} z {1} dokumentów w tym miesiącu." #. placeholder {2}: envelope.title #: packages/lib/server-only/document/resend-document.ts msgid "{0} on behalf of \"{1}\" has invited you to {recipientActionVerb} the document \"{2}\"." -msgstr "Sprawdź i {recipientActionVerb} dokument „{2}” utworzony przez użytkownika {0} z zespołu „{1}”." +msgstr "Sprawdź i {recipientActionVerb} dokument „{2}” utworzony przez użytkownika {0} z zespołu „{1}”." #. placeholder {0}: organisation.name #: apps/remix/app/routes/_authenticated+/o.$orgUrl._index.tsx @@ -379,7 +379,7 @@ msgstr "Użytkownik {inviterName} anulował dokument<0/>„{documentName}”" #. placeholder {0}: _(actionVerb).toLowerCase() #: packages/email/template-components/template-document-invite.tsx msgid "{inviterName} has invited you to {0}<0/>\"{documentName}\"" -msgstr "Sprawdź i {0} dokument „{documentName}}” utworzony przez użytkownika {inviterName}" +msgstr "Sprawdź i {0} dokument „{documentName}” utworzony przez użytkownika {inviterName}" #: packages/email/templates/document-invite.tsx msgid "{inviterName} has invited you to {action} {documentName}" @@ -401,12 +401,12 @@ msgstr "Użytkownik {inviterName} usunął Cię z dokumentu<0/>„{documentName} #. placeholder {1}: envelope.title #: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts msgid "{inviterName} on behalf of \"{0}\" has invited you to {recipientActionVerb} the document \"{1}\"." -msgstr "Sprawdź i {recipientActionVerb} dokument „{1}” utworzony przez użytkownika {inviterName} z zespołu „{0}”." +msgstr "Sprawdź i {recipientActionVerb} dokument „{1}” utworzony przez użytkownika {inviterName} z zespołu „{0}”." #. placeholder {0}: _(actionVerb).toLowerCase() #: packages/email/template-components/template-document-invite.tsx msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {0}<0/>\"{documentName}\"" -msgstr "Sprawdź i {0} dokument<0/>„{documentName}” utworzony przez użytkownika {inviterName} z zespołu „{teamName}”" +msgstr "Sprawdź i {0} dokument<0/>„{documentName}” utworzony przez użytkownika {inviterName} z zespołu „{teamName}”" #: packages/email/templates/document-invite.tsx msgid "{inviterName} on behalf of \"{teamName}\" has invited you to {action} {documentName}" @@ -425,7 +425,7 @@ msgstr "{maximumEnvelopeItemCount, plural, one {Nie możesz przesłać więcej n #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "{organisationClaimCount, plural, one {# Organisation claim} other {# Organisation claims}}" -msgstr "{organisationClaimCount, plural, one {# roszczenie organizacji} few {# roszczenia organizacji} many {# roszczeń organizacji} other {# roszczenia organizacji}}" +msgstr "{organisationClaimCount, plural, one {# subskrypcję organizacji} few {# subskrypcje organizacji} many {# subskrypcji organizacji} other {# subskrypcji organizacji}}" #: apps/remix/app/components/general/direct-template/direct-template-page.tsx msgid "{recipientActionVerb} document" @@ -480,7 +480,7 @@ msgstr "Użytkownik {signerName} odrzucił dokument „{documentName}”." #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "{subscriptionClaimCount, plural, one {# Subscription claim} other {# Subscription claims}}" -msgstr "{subscriptionClaimCount, plural, one {# roszczenie subskrypcji} few {# roszczenia subskrypcji} many {# roszczeń subskrypcji} other {# roszczenia subskrypcji}}" +msgstr "{subscriptionClaimCount, plural, one {# subskrypcję} few {# subskrypcje} many {# subskrypcji} other {# subskrypcji}}" #. placeholder {0}: _(actionVerb).toLowerCase() #: packages/email/template-components/template-document-invite.tsx @@ -505,15 +505,15 @@ msgstr "Użytkownik {user} zatwierdził dokument" #: packages/lib/utils/document-audit-logs.ts msgid "{user} authenticated with the signing provider" -msgstr "{user} został uwierzytelniony u dostawcy podpisu" +msgstr "Użytkownik {user} uwierzytelnił się u dostawcy podpisu" #: packages/lib/utils/document-audit-logs.ts msgid "{user} authorised the remote signature" -msgstr "{user} autoryzował zdalny podpis" +msgstr "Użytkownik {user} autoryzował podpis zdalny" #: packages/lib/utils/document-audit-logs.ts msgid "{user} cancelled the document" -msgstr "{user} anulował dokument" +msgstr "Użytkownik {user} anulował dokument" #: packages/lib/utils/document-audit-logs.ts msgid "{user} CC'd the document" @@ -580,7 +580,7 @@ msgstr "Użytkownik {user} poprosił o kod weryfikacyjny dla dokumentu" #: packages/lib/utils/document-audit-logs.ts msgid "{user} requested a remote signature" -msgstr "{user} zażądał zdalnego podpisu" +msgstr "Użytkownik {user} poprosił o podpis zdalny" #. placeholder {0}: data.recipientEmail #: packages/lib/utils/document-audit-logs.ts @@ -655,7 +655,7 @@ msgstr "Użytkownik {user} wyświetlił dokument" #: packages/lib/utils/document-audit-logs.ts msgid "{user}'s remote signature was applied" -msgstr "Zastosowano zdalny podpis użytkownika {user}" +msgstr "Użytkownik {user} złożył podpis zdalny" #: packages/lib/utils/document-audit-logs.ts msgid "{user}'s signing provider authentication failed" @@ -687,7 +687,7 @@ msgstr "{visibleRows, plural, one {# wynik} few {# wyniki} many {# wyników} oth #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx msgid "<0>\"{0}\" is no longer available to sign" -msgstr "<0>„{0}” nie jest już dostępny do podpisu" +msgstr "Dokument <0>„{0}” nie jest już dostępny do podpisu" #: packages/email/templates/organisation-account-link-confirmation.tsx msgid "<0>{organisationName} has requested to create an account on your behalf." @@ -1272,7 +1272,7 @@ msgstr "Dodaj dokument" #: packages/ui/primitives/document-flow/add-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx msgid "Add a URL to redirect the user to once the document is signed" -msgstr "Dodaj adres URL do przekierowania użytkownika po podpisaniu dokumentu" +msgstr "Dodaj adres URL, na który użytkownik zostanie przekierowany po podpisaniu dokumentu" #: apps/remix/app/components/general/document/document-edit-form.tsx #: apps/remix/app/components/general/template/template-edit-form.tsx @@ -1302,11 +1302,11 @@ msgstr "Dodaj identyfikator zewnętrzny szablonu. Może być używany do identyf #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "Add an optional reason for cancelling these documents" -msgstr "Dodaj opcjonalny powód anulowania tych dokumentów" +msgstr "Dodaj opcjonalny powód anulowania dokumentów" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx msgid "Add an optional reason for cancelling this document" -msgstr "Dodaj opcjonalny powód anulowania tego dokumentu" +msgstr "Dodaj opcjonalny powód anulowania dokumentu" #: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx msgid "Add and configure multiple documents" @@ -1344,7 +1344,7 @@ msgstr "Dodaj domenę" #: apps/remix/app/components/dialogs/email-transport-create-dialog.tsx msgid "Add Email Transport" -msgstr "Dodaj transport e-mailowy" +msgstr "Dodaj dostawcę poczty e-mail" #: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx msgid "Add fields" @@ -1417,7 +1417,7 @@ msgstr "Dodaj domyślny tekst" #: apps/remix/app/components/general/rate-limit-array-input.tsx msgid "Add rate limit" -msgstr "Dodaj limit liczby żądań" +msgstr "Dodaj limit przepustowości" #: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx msgid "Add recipients" @@ -1477,7 +1477,7 @@ msgstr "Dodaj ten adres URL do dozwolonych adresów przekierowania Twojego dosta #: apps/remix/app/components/dialogs/email-transport-create-dialog.tsx msgid "Add transport" -msgstr "Dodaj transport" +msgstr "Dodaj dostawcę poczty e-mail" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Additional brand information to display at the bottom of emails" @@ -1513,7 +1513,7 @@ msgstr "Tylko dla administratorów" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Advanced — Custom CSS" -msgstr "Zaawansowane — własny CSS" +msgstr "Zaawansowane – niestandardowy CSS" #: packages/ui/primitives/document-flow/add-settings.tsx #: packages/ui/primitives/template-flow/add-template-settings.tsx @@ -1559,7 +1559,7 @@ msgstr "Wszystko" #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "All claims" -msgstr "Wszystkie zgłoszenia" +msgstr "Wszystkie subskrypcje" #: apps/remix/app/components/general/app-command-menu.tsx msgid "All documents" @@ -1638,7 +1638,7 @@ msgstr "Odpowiadanie odbiorcom dokumentu bezpośrednio na ten adres e-mail" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "Allow Personal Organisations" -msgstr "Zezwalaj na osobiste organizacje" +msgstr "Zezwól na osobiste organizacje" #: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -1800,7 +1800,7 @@ msgstr "Wystąpił błąd podczas wyłączania użytkownika." #: apps/remix/app/utils/toast-error-messages.ts msgid "An error occurred while distributing the document." -msgstr "Wystąpił błąd podczas dystrybucji dokumentu." +msgstr "Wystąpił błąd podczas wysyłania dokumentu." #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx msgid "An error occurred while enabling direct link signing." @@ -1907,11 +1907,11 @@ msgstr "Wystąpił błąd. Spróbuj ponownie później." #: packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.handler.ts msgid "An organisation has exceeded their fair use limits" -msgstr "Organizacja przekroczyła swoje limity dozwolonego użytkowania" +msgstr "Organizacja przekroczyła limit dozwolonego użytkowania" #: packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.handler.ts msgid "An organisation is nearing their fair use limits" -msgstr "Organizacja zbliża się do swoich limitów dozwolonego użytkowania" +msgstr "Organizacja zbliża się do limit dozwolonego użytkowania" #: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx msgid "An organisation wants to create an account for you. Please review the details below." @@ -2019,7 +2019,7 @@ msgstr "Klucz API" #: apps/remix/app/components/general/claim-limit-fields.tsx msgid "API rate limits" -msgstr "Limity liczby żądań API" +msgstr "Limit przepustowości API" #: apps/remix/app/components/general/organisation-usage-panel.tsx msgid "API requests" @@ -2027,7 +2027,7 @@ msgstr "Żądania API" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "API requests have been temporarily paused" -msgstr "Żądania API zostały tymczasowo wstrzymane." +msgstr "Żądania API zostały tymczasowo wstrzymane" #: apps/remix/app/components/general/settings-nav-desktop.tsx #: apps/remix/app/components/general/settings-nav-mobile.tsx @@ -2039,7 +2039,7 @@ msgstr "Tokeny API" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "API usage is approaching fair use limits" -msgstr "Wykorzystanie API zbliża się do limitów dozwolonego użytkowania" +msgstr "Wykorzystanie API zbliża się do limitu dozwolonego użytkowania" #: apps/remix/app/routes/_authenticated+/admin+/stats.tsx msgid "App Version" @@ -2047,7 +2047,7 @@ msgstr "Wersja aplikacji" #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Applying your signature" -msgstr "Trwa stosowanie Twojego podpisu" +msgstr "Składanie podpisu" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Approaching fair use limit" @@ -2057,7 +2057,7 @@ msgstr "Zbliżasz się do limitu dozwolonego użytkowania" #: packages/email/templates/organisation-limit-alert.tsx #: packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.handler.ts msgid "Approaching Your Plan Limits" -msgstr "Zbliżasz się do limitów swojego planu" +msgstr "Zbliżasz się do limitu planu" #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx @@ -2118,7 +2118,7 @@ msgstr "Czy na pewno chcesz usunąć następującą subskrypcję?" #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "Are you sure you want to delete the following transport?" -msgstr "Czy na pewno chcesz usunąć następujący transport?" +msgstr "Czy na pewno chcesz usunąć następującego dostawcę poczty e-mail?" #: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Are you sure you want to delete this folder?" @@ -2205,7 +2205,7 @@ msgstr "Przygotowujący" #: apps/remix/app/components/embed/multisign/multi-sign-document-list.tsx msgid "Assistants and Copy roles are currently not compatible with the multi-sign experience." -msgstr "Przygotowujący i pobierający kopię nie są kompatybilni z funkcją wielu podpisów." +msgstr "Przygotowujący i pobierający kopię nie są kompatybilni z funkcją wielu podpisów." #: apps/remix/app/components/general/document/document-page-view-recipients.tsx msgid "Assisted" @@ -2328,7 +2328,7 @@ msgstr "Zadania w tle" #: apps/remix/app/components/dialogs/claim-update-dialog.tsx msgid "Backport email transport" -msgstr "Backport transportu e-mailowego" +msgstr "Użyj dostawcy poczty e-mail" #: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx #: apps/remix/app/components/forms/signin.tsx @@ -2345,11 +2345,11 @@ msgstr "Baner został zaktualizowany" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Base background colour." -msgstr "Bazowy kolor tła." +msgstr "Kolor tła." #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Base text colour." -msgstr "Bazowy kolor tekstu." +msgstr "Kolor tekstu." #: packages/email/template-components/template-confirmation-email.tsx msgid "Before you get started, please confirm your email address by clicking the button below:" @@ -2367,7 +2367,7 @@ msgstr "Płatności" #: apps/remix/app/components/forms/public-profile-form.tsx msgid "Bio" -msgstr "Bio" +msgstr "Biografia" #: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx msgid "Black" @@ -2375,7 +2375,7 @@ msgstr "Czarny" #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "Block signups from additional email domains on top of the bundled disposable email list. Subdomains are matched automatically (e.g. blocking \"bad.com\" also blocks \"foo.bad.com\")." -msgstr "Blokuj rejestracje z dodatkowych domen e‑mail ponad dołączoną listę jednorazowych adresów. Subdomeny są dopasowywane automatycznie (np. zablokowanie \"bad.com\" powoduje również zablokowanie \"foo.bad.com\")." +msgstr "Zablokuj rejestracje z konkretnych domen. Subdomeny są blokowane automatycznie (np. domena.pl spowoduje również zablokowanie subdomena.domena.pl). Domeny tymczasowych adresów e-mail znajdują się na tej liście." #: apps/remix/app/components/general/organisation-usage-panel.tsx msgid "Blocked" @@ -2502,6 +2502,7 @@ msgstr "Akceptując prośbę, przyznasz zespołowi {0} następujące uprawnienia #: packages/email/templates/confirm-team-email.tsx msgid "By accepting this request, you will be granting <0>{teamName} access to:" msgstr "Akceptując prośbę, umożliwisz zespołowi <0>{teamName} na:" +msgstr "Akceptując prośbę, umożliwisz zespołowi <0>{teamName}:" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "By deleting this document, the following will occur:" @@ -2711,7 +2712,7 @@ msgstr "Wyśrodkuj" #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx msgid "Change Field Type" -msgstr "Zmień typ pola" +msgstr "Zmień rodzaj pola" #: apps/remix/app/components/general/app-command-menu.tsx msgid "Change language" @@ -2800,7 +2801,7 @@ msgstr "Wybierz..." #: apps/remix/app/components/tables/admin-organisation-stats-table.tsx msgid "Claim" -msgstr "Zgłoszenie" +msgstr "Subskrypcja" #: apps/remix/app/components/general/claim-account.tsx msgid "Claim account" @@ -2830,7 +2831,7 @@ msgstr "Kliknij, aby rozpocząć" #: apps/remix/app/components/general/template/template-page-view-recent-activity.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx msgid "Click here to retry" -msgstr "Kliknij tutaj, aby spróbować ponownie" +msgstr "Kliknij, aby spróbować ponownie" #: apps/remix/app/components/dialogs/organisation-member-invite-dialog.tsx msgid "Click here to upload" @@ -2902,7 +2903,7 @@ msgstr "Zwiń panel boczny" #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "Comma-separated list of email domains to block from signing up." -msgstr "Lista domen e‑mail do zablokowania przy rejestracji, oddzielonych przecinkami." +msgstr "Lista zablokowanych domen do rejestracji oddzielona przecinkami." #: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx msgid "Communication" @@ -3018,7 +3019,7 @@ msgstr "Skonfiguruj ustawienia zabezpieczeń dokumentu." #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx msgid "Configure signing reminder settings for the document." -msgstr "Skonfiguruj przypomnienia o podpisaniu dokumentu." +msgstr "Skonfiguruj ustawienia przypomnień o podpisaniu dokumentu." #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "Configure template" @@ -3050,7 +3051,7 @@ msgstr "Skonfiguruj role w zespole dla każdego użytkownika" #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx msgid "Configure when and how often reminder emails are sent to recipients who have not yet completed signing. Uses the team default when set to inherit." -msgstr "Skonfiguruj, kiedy i jak często przypomnienia o podpisaniu będą wysyłane do odbiorców, którzy nie zakończyli dokumentu. Opcja dziedziczenia korzysta z domyślnych ustawień zespołu." +msgstr "Skonfiguruj, kiedy i jak często przypomnienia e-mail są wysyłane do odbiorców, którzy jeszcze nie ukończyli podpisywania. Gdy ustawione na dziedziczenie, używane są domyślne ustawienia zespołu." #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx #: apps/remix/app/components/dialogs/sign-field-checkbox-dialog.tsx @@ -3179,7 +3180,7 @@ msgstr "Wybierz język dokumentu, powiadomień i certyfikatu, który jest dołą #: apps/remix/app/components/forms/document-preferences-form.tsx msgid "Controls when and how often reminder emails are sent to recipients who have not yet completed signing." -msgstr "Skonfiguruj, kiedy i jak często przypomnienia o podpisaniu będą wysyłane do odbiorców, którzy nie zakończyli dokumentu." +msgstr "Określa, kiedy i jak często przypomnienia e-mail są wysyłane do odbiorców, którzy jeszcze nie ukończyli podpisywania." #: apps/remix/app/components/forms/document-preferences-form.tsx msgid "Controls whether the audit logs will be included in the document when it is downloaded. The audit logs can still be downloaded from the logs page separately." @@ -3272,7 +3273,7 @@ msgstr "Kopiuj wartość" #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Counter reset." -msgstr "Licznik zresetowany." +msgstr "Licznik został zresetowany." #: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx #: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx @@ -3304,7 +3305,7 @@ msgstr "Utwórz nową organizację z planem {planName}. Zachowaj obecną organiz #: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx msgid "Create a new user. A welcome email will be sent with a link to set their password." -msgstr "Utwórz nowego użytkownika. Zostanie wysłany e‑mail powitalny z łączem do ustawienia hasła." +msgstr "Utwórz nowego użytkownika. Wiadomość powitalna zostanie wysłana z linkiem do ustawienia hasła." #: apps/remix/app/routes/_authenticated+/o.$orgUrl.support.tsx msgid "Create a support ticket" @@ -3541,7 +3542,7 @@ msgstr "Tworzenie szablonu" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "CSS rules were dropped during sanitisation" -msgstr "Niektóre reguły CSS zostały odrzucone podczas oczyszczania" +msgstr "Reguły CSS zostały usunięte podczas oczyszczenia kodu" #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "CSV Structure" @@ -3565,7 +3566,7 @@ msgstr "Obecni odbiorcy:" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Current usage against organisation limits." -msgstr "Obecne użycie względem limitów organizacji." +msgstr "Obecne wykorzystanie względem limitów organizacji." #: apps/remix/app/components/general/teams/team-inherit-member-alert.tsx msgid "Currently all organisation members can access this team" @@ -3586,7 +3587,7 @@ msgstr "Niestandardowy plik {0} MB" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Custom CSS is sanitised on save. Layout-breaking properties, remote URLs, and pseudo-elements are stripped automatically. Any rules dropped during sanitisation will be shown after you save." -msgstr "Niestandardowy kod CSS jest czyszczony podczas zapisywania. Właściwości mogące zepsuć układ, zdalne adresy URL oraz pseudo‑elementy są automatycznie usuwane. Wszystkie reguły odrzucone w trakcie czyszczenia zostaną wyświetlone po zapisaniu." +msgstr "Niestandardowy CSS jest oczyszczany podczas zapisywania. Właściwości psujące układ graficzny, zdalne adresy URL oraz pseudoelementy są automatycznie usuwane. Reguły usunięte podczas oczyszczania kodu zostaną wyświetlone po zapisaniu zmian." #: packages/ui/components/document/expiration-period-picker.tsx msgid "Custom duration" @@ -3594,7 +3595,7 @@ msgstr "Niestandardowy czas" #: packages/ui/components/document/reminder-settings-picker.tsx msgid "Custom interval" -msgstr "Niestandardowy czas" +msgstr "Własny interwał" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups._index.tsx msgid "Custom Organisation Groups" @@ -3602,11 +3603,11 @@ msgstr "Niestandardowe grupy w organizacji" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Customise the colours used on your signing pages." -msgstr "Dostosuj kolory używane na stronach podpisywania." +msgstr "Dostosuj kolory na stronach podpisywania." #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Danger Zone" -msgstr "Strefa zagrożenia" +msgstr "Niebezpieczna strefa" #: apps/remix/app/components/general/app-command-menu.tsx msgid "Dark Mode" @@ -3662,11 +3663,11 @@ msgstr "Odrzuć" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Default (system mailer)" -msgstr "Domyślny (systemowy mailer)" +msgstr "Domyślny (systemowy)" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Default border colour." -msgstr "Domyślny kolor obramowania." +msgstr "Kolor obramowania." #: apps/remix/app/components/forms/document-preferences-form.tsx msgid "Default Date Format" @@ -3686,7 +3687,7 @@ msgstr "Domyślny adres e-mail" #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Default Email Settings" -msgstr "Domyślne ustawienia powiadomień" +msgstr "Domyślne ustawienia adresu e-mail" #: apps/remix/app/components/forms/document-preferences-form.tsx msgid "Default Envelope Expiration" @@ -3850,7 +3851,7 @@ msgstr "Usuń domenę" #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "Delete Email Transport" -msgstr "Usuń transport e-mailowy" +msgstr "Usuń dostawcę poczty e-mail" #: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx msgid "Delete Envelope" @@ -3926,7 +3927,7 @@ msgstr "Usuwanie konta..." #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "Deletion scheduled" -msgstr "Usunięcie zaplanowane" +msgstr "Usunięcie zostało zaplanowane" #: apps/remix/app/components/general/webhook-logs-sheet.tsx msgid "Destination" @@ -3997,7 +3998,7 @@ msgstr "Urządzenie" #: packages/email/template-components/template-footer.tsx msgid "Did not expect this email? <0>Click here to report the sender. Never sign a document you don't recognize or weren't expecting." -msgstr "Nie spodziewasz się tej wiadomości e-mail? <0>Kliknij tutaj, aby zgłosić nadawcę. Nigdy nie podpisuj dokumentu, którego nie rozpoznajesz lub którego się nie spodziewałeś(-aś)." +msgstr "Wiadomość nie była przez Ciebie oczekiwana? <0>Kliknij, aby zgłosić nadawcę. Nigdy nie podpisuj dokumentu, którego nie rozpoznajesz." #: packages/email/templates/reset-password.tsx msgid "Didn't request a password change? We are here to help you secure your account, just <0>contact us." @@ -4056,7 +4057,7 @@ msgstr "Bezpośredni link do szablonu został usunięty" #. placeholder {1}: quota.directTemplates #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx msgid "Direct template link usage exceeded ({0}/{1})" -msgstr "Przekroczono limit użycia bezpośrednich linków do szablonu ({0} / {1})" +msgstr "Przekroczono limit wykorzystania bezpośrednich linków do szablonu ({0} / {1})" #: apps/remix/app/components/forms/editor/editor-field-checkbox-form.tsx #: apps/remix/app/components/forms/editor/editor-field-radio-form.tsx @@ -4123,7 +4124,7 @@ msgstr "Wyświetlać Twoją nazwę i adres e-mail w dokumentach" #: apps/remix/app/components/forms/signup.tsx msgid "Disposable email addresses are not allowed. Please sign up with a permanent email address." -msgstr "Adresy e‑mail jednorazowego użytku nie są dozwolone. Zarejestruj się, używając stałego adresu e‑mail." +msgstr "Tymczasowe adresy e-mail nie są dozwolone. Zarejestruj się za pomocą standardowego adresu e-mail." #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Distribute Document" @@ -4205,12 +4206,12 @@ msgstr "Dokument został zatwierdzony" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx #: apps/remix/app/components/general/document/document-status.tsx msgid "Document cancelled" -msgstr "Dokument został anulowany" +msgstr "Anulowano dokument" #: packages/lib/utils/document-audit-logs.ts msgctxt "Audit log format" msgid "Document cancelled" -msgstr "Dokument został anulowany" +msgstr "Anulowano dokument" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx @@ -4242,7 +4243,7 @@ msgstr "Dokument został zakończony!" #: apps/remix/app/utils/toast-error-messages.ts msgid "Document conversion is temporarily unavailable. Please try again shortly or upload a PDF." -msgstr "Konwersja dokumentów jest tymczasowo niedostępna. Spróbuj ponownie za chwilę lub prześlij plik PDF." +msgstr "Konwertowanie dokumentu jest niedostępne. Spróbuj ponownie później lub prześlij plik PDF." #: apps/remix/app/components/dialogs/template-use-dialog.tsx msgid "Document created" @@ -4282,7 +4283,7 @@ msgstr "Tworzenie dokumentu" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Document creation has been temporarily paused" -msgstr "Tworzenie dokumentów zostało tymczasowo wstrzymane." +msgstr "Tworzenie dokumentów zostało tymczasowo wstrzymane" #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx @@ -4407,7 +4408,7 @@ msgstr "Ustawienia dokumentu zostały zaktualizowane" #: apps/remix/app/components/general/claim-limit-fields.tsx msgid "Document rate limits" -msgstr "Limity liczby dokumentów" +msgstr "Limit przepustowości dokumentów" #: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx #: apps/remix/app/components/general/document/document-status.tsx @@ -4426,7 +4427,7 @@ msgstr "Zmieniono nazwę dokumentu" #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx msgid "Document resent" -msgstr "Dokument został ponownie wysłany" +msgstr "Wysłano ponownie dokument" #: apps/remix/app/components/general/document/document-edit-form.tsx msgid "Document sent" @@ -4453,7 +4454,7 @@ msgstr "Zaktualizowano metodę uwierzytelniania odbiorcy do dokumentu" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "Document signing process will be cancelled" -msgstr "Proces podpisywania dokumentu zostanie anulowany" +msgstr "Podpisywanie dokumentu zostanie anulowane" #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx msgid "Document status" @@ -4505,7 +4506,7 @@ msgstr "Dokument został przesłany" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Document usage is approaching fair use limits" -msgstr "Wykorzystanie dokumentów zbliża się do limitów dozwolonego użytkowania" +msgstr "Wykorzystanie dokumentów zbliża się do limitu dozwolonego użytkowania" #: packages/lib/utils/document-audit-logs.ts msgctxt "Audit log format" @@ -4614,7 +4615,7 @@ msgstr "Dokumenty, które zostały podpisane przez wszystkich odbiorców, ale ni #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "Documents, emails and api values may not be accurate since they record the amount of times the action was attempted. Meaning these values may go over the actual quota, get rejected, and will still be recorded." -msgstr "Dokumenty, e-maile i wartości API mogą nie być dokładne, ponieważ rejestrują liczbę prób wykonania danej akcji. Oznacza to, że te wartości mogą przekraczać faktyczny limit, być odrzucane, a mimo to nadal będą zapisywane." +msgstr "Wartości dokumentów, wiadomości i API mogą nie być dokładne, ponieważ rejestrują liczbę prób wykonania danej akcji. Oznacza to, że wartości te mogą przekroczyć rzeczywisty limit, zostać odrzucone, a mimo to nadal będą rejestrowane." #: apps/remix/app/components/tables/organisation-email-domains-table.tsx #: apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx @@ -4708,7 +4709,7 @@ msgstr "Szkice dokumentów" #: packages/ui/primitives/document-dropzone.tsx msgid "Drag & drop your document here." -msgstr "Przeciągnij i upuść tutaj swój dokument." +msgstr "Przeciągnij i upuść dokument." #: apps/remix/app/components/embed/authoring/configure-document-upload.tsx msgid "Drag and drop or click to upload" @@ -4716,7 +4717,7 @@ msgstr "Przeciągnij i upuść lub kliknij, aby przesłać" #: apps/remix/app/components/general/envelope/envelope-drop-zone-wrapper.tsx msgid "Drag and drop your document here" -msgstr "Przeciągnij i upuść tutaj swój dokument" +msgstr "Przeciągnij i upuść dokument" #: packages/lib/constants/document.ts #: packages/ui/primitives/signature-pad/signature-pad.tsx @@ -4806,7 +4807,7 @@ msgstr "Np. 100" #: apps/remix/app/components/forms/email-transport-form.tsx msgid "e.g. Resend (free plans)" -msgstr "np. Resend (plany bezpłatne)" +msgstr "np. Resend (plan darmowy)" #: apps/remix/app/components/general/document/document-page-view-button.tsx #: apps/remix/app/components/general/document/document-page-view-dropdown.tsx @@ -4824,7 +4825,7 @@ msgstr "Edytuj" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx msgid "Edit Email Transport" -msgstr "Edytuj transport e-mailowy" +msgstr "Edytuj dostawcę poczty e-mail" #: apps/remix/app/components/dialogs/envelope-item-edit-dialog.tsx msgid "Edit Item" @@ -4926,11 +4927,11 @@ msgstr "Adres e-mail już istnieje" #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "Email Blocklist" -msgstr "Bloklista e‑mail" +msgstr "Lista zablokowanych domen" #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "Email Blocklist Updated" -msgstr "Zaktualizowano bloklistę e‑mail" +msgstr "Lista została zaktualizowana" #: apps/remix/app/routes/_unauthenticated+/verify-email.$token.tsx msgid "Email Confirmed!" @@ -4942,11 +4943,11 @@ msgstr "Adres e-mail został utworzony" #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx msgid "Email distribution needs to be enabled in the general settings tab to configure recipient email related settings." -msgstr "Aby móc skonfigurować ustawienia dotyczące wiadomości e-mail odbiorców, należy włączyć dystrybucję e-mail w zakładce ustawień ogólnych." +msgstr "Aby skonfigurować wiadomości, musisz włączyć dystrybucję dokumentu za pomocą poczty e-mail." #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Email document settings" -msgstr "Ustawienia powiadomień" +msgstr "Ustawienia wysyłki dokumentu e‑mailem" #: apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx msgid "Email Domain" @@ -4995,7 +4996,7 @@ msgstr "Ustawienia adresu e-mail zostały zaktualizowane" #: apps/remix/app/components/general/claim-limit-fields.tsx msgid "Email rate limits" -msgstr "Limity liczby wiadomości e-mail" +msgstr "Limit przepustowości wiadomości" #: packages/ui/components/document/document-email-checkboxes.tsx msgid "Email recipients when a pending document is deleted" @@ -5031,7 +5032,7 @@ msgstr "Adres e-mail nadawcy" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Email sending has been temporarily paused" -msgstr "Wysyłanie wiadomości e-mail zostało tymczasowo wstrzymane." +msgstr "Wysyłanie wiadomości zostało tymczasowo wstrzymane" #: packages/lib/utils/document-audit-logs.ts msgctxt "Audit log format" @@ -5048,7 +5049,7 @@ msgstr "Ustawienia adresu e-mail" #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "Email the organisation owner to notify them of the deletion." -msgstr "Wyślij e-mail do właściciela organizacji, aby powiadomić go o usunięciu." +msgstr "Wyślij właścicielowi organizacji powiadomienie o usunięciu." #: packages/ui/components/document/document-email-checkboxes.tsx msgid "Email the owner when a document is created from a direct template" @@ -5069,16 +5070,16 @@ msgstr "Wyślij podpisującemu wiadomość, jeśli dokument jest nadal oczekują #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Email transport" -msgstr "Transport e-mailowy" +msgstr "Dostawca poczty e-mail" #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx msgid "Email Transports" -msgstr "Transporty e-mailowe" +msgstr "Dostawcy poczty e-mail" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Email usage is approaching fair use limits" -msgstr "Wykorzystanie e‑maili zbliża się do limitów dozwolonego użytkowania" +msgstr "Wykorzystanie wiadomości zbliża się do limitu dozwolonego użytkowania" #: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx msgid "Email verification" @@ -5109,7 +5110,7 @@ msgstr "Osadzanie dokumentów, 5 użytkowników i więcej" #: apps/remix/app/components/general/claim-limit-fields.tsx #: apps/remix/app/components/general/claim-limit-fields.tsx msgid "Empty = Unlimited, 0 = Blocked" -msgstr "Puste = Bez limitu, 0 = Zablokowane" +msgstr "Puste = bez ograniczeń, 0 = zablokowane" #: packages/ui/primitives/document-flow/add-fields.tsx msgid "Empty field" @@ -5216,7 +5217,7 @@ msgstr "Załączniki" #: apps/remix/app/components/forms/email-transport-form.tsx msgid "Endpoint (optional)" -msgstr "Endpoint (opcjonalnie)" +msgstr "Punkt końcowy (opcjonalnie)" #: packages/lib/constants/i18n.ts msgid "English" @@ -5578,7 +5579,7 @@ msgstr "Nie udało się utworzyć zgłoszenia" #: apps/remix/app/components/dialogs/email-transport-create-dialog.tsx msgid "Failed to create transport." -msgstr "Nie udało się utworzyć transportu." +msgstr "Nie udało się utworzyć dostawcy poczty e-mail." #: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "Failed to delete folder" @@ -5590,7 +5591,7 @@ msgstr "Nie udało się usunąć subskrypcji." #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "Failed to delete transport." -msgstr "Nie udało się usunąć transportu." +msgstr "Nie udało się usunąć dostawcy poczty e-mail." #: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx msgid "Failed to download audit logs. Please try again later." @@ -5634,7 +5635,7 @@ msgstr "Nie udało się zapisać ustawień." #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx msgid "Failed to save transport." -msgstr "Nie udało się zapisać transportu." +msgstr "Nie udało się zapisać dostawcy poczty e-mail." #: apps/remix/app/components/dialogs/session-logout-all-dialog.tsx msgid "Failed to sign out all sessions" @@ -5695,7 +5696,7 @@ msgstr "Niepowodzenie: {failedCount}" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx #: apps/remix/app/utils/toast-error-messages.ts msgid "Fair use limit exceeded" -msgstr "Przekroczono limit dozwolonego użycia." +msgstr "Przekroczono limit dozwolonego użytkowania" #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -5709,7 +5710,7 @@ msgstr "Funkcje" #: apps/remix/app/components/dialogs/admin-organisation-sync-subscription-dialog.tsx msgid "Fetch the latest subscription data from Stripe and apply it to this organisation." -msgstr "Pobierz najnowsze dane subskrypcji ze Stripe i zastosuj je do tej organizacji." +msgstr "Pobierz najnowsze dane subskrypcji z Stripe i zastosuj je w organizacji." #: packages/ui/primitives/document-flow/field-items-advanced-settings/text-field.tsx msgid "Field character limit" @@ -5785,7 +5786,7 @@ msgstr "Plik nie może być większy niż {APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB" #: apps/remix/app/components/dialogs/email-transport-create-dialog.tsx msgid "Fill in the details to create a new email transport." -msgstr "Uzupełnij szczegóły, aby utworzyć nowy transport e-mailowy." +msgstr "Uzupełnił szczegóły, aby utworzyć nowego dostawcę poczty e-mail." #: apps/remix/app/components/dialogs/claim-create-dialog.tsx msgid "Fill in the details to create a new subscription claim." @@ -5797,7 +5798,7 @@ msgstr "Filtruj według statusu" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Focus ring colour." -msgstr "Kolor obramowania aktywnego elementu (focus ring)." +msgstr "Kolor obramowania zaznaczenia." #: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx msgid "Folder" @@ -5857,7 +5858,7 @@ msgstr "Na przykład, jeśli subskrypcja ma nową flagę „FLAG_1” ustawioną #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Foreground" -msgstr "Kolor pierwszego planu" +msgstr "Tekst" #: apps/remix/app/routes/_unauthenticated+/check-email.tsx msgid "Forgot password" @@ -5891,7 +5892,7 @@ msgstr "Darmowa" #: apps/remix/app/components/tables/user-billing-organisations-table.tsx msgctxt "Subscription status" msgid "Free (Pending)" -msgstr "Darmowy (oczekujący)" +msgstr "Darmowa (oczekująca)" #: packages/lib/utils/fields.ts #: packages/ui/primitives/document-flow/types.ts @@ -6147,11 +6148,11 @@ msgstr "Cześć, jestem Timur" #: packages/email/template-components/template-document-reminder.tsx msgid "Hi {recipientName}," -msgstr "Cześć {recipientName}," +msgstr "Cześć, {recipientName}," #: packages/email/templates/bulk-send-complete.tsx msgid "Hi {userName}," -msgstr "Cześć {userName}," +msgstr "Cześć, {userName}" #: packages/email/template-components/template-access-auth-2fa.tsx msgid "Hi {userName}, you need to enter a verification code to complete the document \"{documentTitle}\"." @@ -6159,7 +6160,7 @@ msgstr "Cześć {userName}, wpisz kod weryfikacyjny, aby zakończyć dokument #: packages/email/templates/reset-password.tsx msgid "Hi, {userName} <0>({userEmail})" -msgstr "Cześć {userName} <0>({userEmail})" +msgstr "Cześć, {userName} <0>({userEmail})" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx #: apps/remix/app/components/tables/documents-table-action-dropdown.tsx @@ -6201,7 +6202,7 @@ msgstr "Czas, w którym odbiorcy muszą zakończyć dokument. Opcja dziedziczeni #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Human verification required" -msgstr "Wymagana weryfikacja za pomocą CAPTCHA" +msgstr "Weryfikacja antyspamowa jest wymagana" #: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx msgid "I agree to link my account with this organization" @@ -6230,7 +6231,7 @@ msgstr "Muszę otrzymać kopię dokumentu" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx #: packages/lib/server-only/pdf/render-certificate.ts msgid "I am the owner of this document" -msgstr "Jestem właścicielem tego dokumentu" +msgstr "Jestem właścicielem dokumentu" #: apps/remix/app/routes/_unauthenticated+/o.$orgUrl.signin.tsx msgid "I understand that I am providing my credentials to a 3rd party service configured by this organisation" @@ -6270,11 +6271,11 @@ msgstr "Jeśli korzystasz ze środowiska „staging”, ustaw właściwość hos #: apps/remix/app/routes/_recipient+/report.$token.tsx msgid "If you did not expect this email or believe it is spam, you can report the sender to our team for review." -msgstr "Jeśli nie spodziewałeś(-aś) się tej wiadomości e-mail lub uważasz, że to spam, możesz zgłosić nadawcę do naszego zespołu do weryfikacji." +msgstr "Jeśli wiadomość nie była przez Ciebie oczekiwana lub uważasz, że jest to spam, zgłoś nadawcę do naszego zespołu." #: packages/email/template-components/template-admin-user-created.tsx msgid "If you didn't expect this account or have any questions, please <0>contact support." -msgstr "Jeśli nie spodziewałeś(-aś) się tego konta lub masz jakiekolwiek pytania, <0>skontaktuj się z pomocą techniczną." +msgstr "Jeśli utworzenie konta jest błędem lub masz pytania, <0>skontaktuj się z pomocą techniczną." #: packages/email/template-components/template-access-auth-2fa.tsx msgid "If you didn't request this verification code, you can safely ignore this email." @@ -6290,7 +6291,7 @@ msgstr "Jeśli nie znajdziesz wiadomości z linkiem potwierdzającym, możesz po #: packages/email/templates/organisation-limit-alert.tsx msgid "If you expect to need higher limits, please contact support at {SUPPORT_EMAIL} and we will review your account." -msgstr "Jeśli spodziewasz się potrzeby wyższych limitów, skontaktuj się z działem wsparcia pod adresem {SUPPORT_EMAIL}, a my przejrzymy Twoje konto." +msgstr "Jeśli potrzebujesz wyższych limitów, skontaktuj się z pomocą techniczną pod adresem {SUPPORT_EMAIL}." #: apps/remix/app/components/forms/2fa/enable-authenticator-app-dialog.tsx msgid "If your authenticator app does not support QR codes, you can use the following code instead:" @@ -6536,7 +6537,7 @@ msgstr "Adres IP" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Irreversible actions for this organisation" -msgstr "Nieodwracalne działania dla tej organizacji" +msgstr "Nieodwracalne akcje dla organizacji" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "Issuer URL" @@ -6712,7 +6713,7 @@ msgstr "Pozostaw puste, aby odziedziczyć z organizacji." #: apps/remix/app/components/forms/email-transport-form.tsx msgid "Leave blank to keep current" -msgstr "Pozostaw puste, aby zachować obecne" +msgstr "Pozostaw puste, aby nie zmieniać" #: apps/remix/app/components/dialogs/organisation-leave-dialog.tsx msgid "Leave organisation" @@ -6882,7 +6883,7 @@ msgstr "Zarządzaj niestandardowym logowaniem SSO dla swojej organizacji." #: apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx msgid "Manage all email transports" -msgstr "Zarządzaj wszystkimi transportami e-mailowymi" +msgstr "Zarządzaj wszystkimi dostawcami poczty e-mail" #: apps/remix/app/routes/_authenticated+/settings+/organisations.tsx msgid "Manage all organisations you are currently associated with." @@ -7066,7 +7067,7 @@ msgstr "MAU (zalogowani)" #: apps/remix/app/components/forms/editor/editor-field-number-form.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx msgid "Max" -msgstr "Max" +msgstr "Maks." #: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx msgid "Maximum file size: 4MB. Maximum 100 rows per upload. Blank values will use template defaults." @@ -7075,7 +7076,7 @@ msgstr "Maksymalny rozmiar pliku to 4 MB. Możesz przesłać maksymalnie 100 wie #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Maximum number of recipients per document allowed. 0 = Unlimited" -msgstr "Maksymalna dozwolona liczba odbiorców na dokument. 0 = Bez limitu" +msgstr "Maksymalna dozwolona liczba odbiorców dokumentu. 0 = bez ograniczeń" #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -7151,7 +7152,7 @@ msgstr "Środek" #: apps/remix/app/components/forms/editor/editor-field-number-form.tsx #: packages/ui/primitives/document-flow/field-items-advanced-settings/number-field.tsx msgid "Min" -msgstr "Min" +msgstr "Min." #: apps/remix/app/components/general/admin-license-status-banner.tsx msgid "Missing License - Your Documenso instance is using features that require a license." @@ -7168,7 +7169,7 @@ msgstr "Edytuj odbiorców" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx msgid "Modify the details of the email transport." -msgstr "Zmień szczegóły transportu e-mailowego." +msgstr "Edytuj szczegóły dostawcy poczty e-mail." #: apps/remix/app/components/dialogs/claim-update-dialog.tsx msgid "Modify the details of the subscription claim." @@ -7197,7 +7198,7 @@ msgstr "Miesięczny limit dokumentów" #: apps/remix/app/components/general/claim-limit-fields.tsx msgid "Monthly email quota" -msgstr "Miesięczny limit e‑maili" +msgstr "Miesięczny limit wiadomości" #: apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx #: apps/remix/app/components/dialogs/folder-move-dialog.tsx @@ -7298,7 +7299,7 @@ msgstr "Ustawienia nazwy" #: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx msgid "Need to sign documents?" -msgstr "Potrzebujesz podpisać dokumenty?" +msgstr "Potrzebujesz podpisywać dokumenty?" #: packages/ui/components/recipient/recipient-role-select.tsx msgid "Needs to approve" @@ -7406,7 +7407,7 @@ msgstr "Brak włączonych funkcji" #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx msgid "No field type matching this description was found." -msgstr "Nie znaleziono typu pola pasującego do tego opisu." +msgstr "Nie znaleziono pola pasującego do tego opisu." #: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx msgid "No fields were detected in your document." @@ -7517,7 +7518,7 @@ msgstr "Nie znaleziono pola podpisu" #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "No signing credentials available" -msgstr "Brak dostępnych poświadczeń do podpisu" +msgstr "Brak dostępnych certyfikatów podpisu" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "No Stripe customer attached" @@ -7595,7 +7596,7 @@ msgstr "Nieobsługiwane" #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "Nothing cancelled" -msgstr "Nic nie zostało anulowane" +msgstr "Nic nie anulowano" #: apps/remix/app/components/tables/documents-table-empty-state.tsx #: apps/remix/app/components/tables/documents-table-empty-state.tsx @@ -7628,12 +7629,12 @@ msgstr "Liczba musi być w formacie {numberFormat}" #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Number of members allowed. 0 = Unlimited" -msgstr "Liczba dozwolonych użytkowników. 0 = Bez ograniczeń" +msgstr "Liczba dozwolonych użytkowników. 0 = bez ograniczeń" #: apps/remix/app/components/forms/subscription-claim-form.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Number of teams allowed. 0 = Unlimited" -msgstr "Liczba dozwolonych zespołów. 0 = Bez ograniczeń" +msgstr "Liczba dozwolonych zespołów. 0 = bez ograniczeń" #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx msgid "Number Settings" @@ -7704,7 +7705,7 @@ msgstr "Dozwolone są tylko pliki PDF" #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "Only pending documents you have permission to manage will be cancelled." -msgstr "Tylko oczekujące dokumenty, do których masz uprawnienia do zarządzania, zostaną anulowane." +msgstr "Tylko oczekujące dokumenty, do których masz uprawnienia, zostaną anulowane." #: apps/remix/app/components/general/generic-error-layout.tsx #: apps/remix/app/components/general/generic-error-layout.tsx @@ -7768,7 +7769,7 @@ msgstr "Organizacja" #: packages/lib/server-only/organisation/delete-organisation-email.ts msgid "Organisation \"{organisationName}\" has been deleted" -msgstr "Organizacja \"{organisationName}\" została usunięta" +msgstr "Organizacja „{organisationName}” została usunięta" #: packages/lib/constants/organisations-translations.ts msgid "Organisation Admin" @@ -7844,7 +7845,7 @@ msgstr "Organizacja nie została znaleziona" #: packages/email/templates/organisation-limit-alert.tsx #: packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.handler.ts msgid "Organisation Review Required" -msgstr "Wymagana weryfikacja organizacji" +msgstr "Weryfikacja organizacji jest wymagana" #: apps/remix/app/components/dialogs/organisation-group-create-dialog.tsx #: apps/remix/app/routes/_authenticated+/admin+/teams.$id.tsx @@ -7894,11 +7895,11 @@ msgstr "Adres URL organizacji" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Organisation usage" -msgstr "Użycie organizacji" +msgstr "Wykorzystanie organizacji" #: apps/remix/app/routes/_authenticated+/admin+/teams.$id.tsx msgid "Organisation-level pending invites for this team's parent organisation." -msgstr "Oczekujące zaproszenia na poziomie organizacji zespołu." +msgstr "Oczekujące zaproszenia na poziomie organizacji dla organizacji nadrzędnej tego zespołu." #: apps/remix/app/components/general/org-menu-switcher.tsx #: apps/remix/app/components/general/settings-nav-desktop.tsx @@ -7915,7 +7916,7 @@ msgstr "Organizacje użytkownika." #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Organisations without a transport use the system default mailer." -msgstr "Organizacje bez zdefiniowanego transportu używają domyślnego systemowego mailera." +msgstr "Organizacje bez dostawcy poczty e-mail używają domyślnego systemu wysyłki." #: apps/remix/app/components/general/folder/folder-card.tsx msgid "Organise your documents" @@ -7966,15 +7967,15 @@ msgstr "Właściciel" #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Owner document completed" -msgstr "Powiadomiono właściciela o zakończeniu dokumentu" +msgstr "Dokument właściciela zakończony" #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Owner document created" -msgstr "Powiadomiono właściciela o utworzeniu dokumentu" +msgstr "Dokument właściciela utworzony" #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Owner recipient expired" -msgstr "Powiadomiono właściciela o minięciu czasu na podpisanie dokumentu" +msgstr "Odbiorca właściciela wygasł" #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx msgid "Ownership transferred to {organisationMemberName}." @@ -7998,7 +7999,7 @@ msgstr "Opłacona" #: apps/remix/app/components/dialogs/envelope-download-dialog.tsx msgctxt "Partially signed document (adjective)" msgid "Partial" -msgstr "Częściowo podpisany" +msgstr "Częściowy" #: apps/remix/app/components/forms/signin.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-dialog.tsx @@ -8096,7 +8097,7 @@ msgstr "Zaległa płatność" #: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx #: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx msgid "Payment required" -msgstr "Wymagana płatność" +msgstr "Płatność jest wymagana" #: apps/remix/app/components/dialogs/envelope-download-dialog.tsx msgid "PDF Document" @@ -8132,7 +8133,7 @@ msgstr "Oczekujące dokumenty" #: apps/remix/app/components/dialogs/envelopes-bulk-delete-dialog.tsx msgid "Pending documents will have their signing process cancelled" -msgstr "Proces podpisywania oczekujących dokumentów zostanie anulowany" +msgstr "Podpisywanie oczekujących dokumentów zostanie anulowane" #: apps/remix/app/components/general/organisations/organisation-invitations.tsx msgid "Pending invitations" @@ -8163,7 +8164,7 @@ msgstr "Okres" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Permanently delete this organisation. Documents will be orphaned (not deleted) so they remain accessible via the deleted-account service account." -msgstr "Trwale usuń tę organizację. Dokumenty staną się osierocone (nieusunięte), aby pozostały dostępne za pośrednictwem konta serwisowego usuniętego konta." +msgstr "Usuń trwale organizację. Dokumenty zostaną osierocone (nie usunięte), więc będą przypisane do konta serwisowego usuniętego konta." #: apps/remix/app/components/tables/user-organisations-table.tsx msgctxt "Personal organisation (adjective)" @@ -8215,7 +8216,7 @@ msgstr "Domyślny tekst" #: apps/remix/app/components/forms/subscription-claim-form.tsx msgid "Plans without a transport use the system default mailer." -msgstr "Plany bez zdefiniowanego transportu używają domyślnego systemowego mailera." +msgstr "Plany bez dostawcy poczty e-mail używają domyślnego systemu wysyłki." #. placeholder {0}: _(actionVerb).toLowerCase() #: packages/email/template-components/template-document-invite.tsx @@ -8259,7 +8260,7 @@ msgstr "Wybierz nowe hasło" #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Please complete the CAPTCHA challenge before signing in." -msgstr "Przed zalogowaniem się dokończ wyzwanie CAPTCHA." +msgstr "Zakończ weryfikację CAPTCHA przed logowaniem." #: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx #: apps/remix/app/components/general/document-signing/document-signing-mobile-widget.tsx @@ -8286,7 +8287,7 @@ msgstr "Jeśli masz pytania, skontaktuj się z <0>pomocą techniczną." #: packages/email/templates/organisation-limit-alert.tsx msgid "Please contact support at {SUPPORT_EMAIL} and we will review your account." -msgstr "Skontaktuj się z pomocą techniczną pod adresem {SUPPORT_EMAIL}, a my zweryfikujemy Twoje konto." +msgstr "Skontaktuj się z pomocą techniczną {SUPPORT_EMAIL}. Sprawdzimy Twoje konto." #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "Please contact support if you would like to revert this action." @@ -8298,7 +8299,7 @@ msgstr "Skontaktuj się z właścicielem dokumentu, aby uzyskać pomoc." #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Please don't close this tab. The signing provider is finalising your signature." -msgstr "Prosimy nie zamykać tej karty. Dostawca podpisu finalizuje Twój podpis." +msgstr "Nie zamykaj karty. Dostawca podpisu finalizuje podpis." #: apps/remix/app/components/forms/token.tsx msgid "Please enter a meaningful name for your token. This will help you identify it later." @@ -8419,7 +8420,7 @@ msgstr "Spróbuj ponownie i upewnij się, że adres e-mail jest prawidłowy." #: apps/remix/app/components/general/pdf-viewer/pdf-viewer-states.tsx msgid "Please try again or contact our support." -msgstr "Spróbuj ponownie lub skontaktuj się z naszym wsparciem." +msgstr "Spróbuj ponownie lub skontaktuj się z pomocą techniczną" #. placeholder {0}: `'${t(deleteMessage)}'` #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx @@ -8507,15 +8508,15 @@ msgstr "Podgląd podpisanego dokumentu z domyślnymi opcjami" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Primary" -msgstr "Kolor główny" +msgstr "Główny" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Primary action colour." -msgstr "Kolor głównej akcji." +msgstr "Kolor akcji." #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Primary Foreground" -msgstr "Kolor pierwszego planu dla koloru głównego" +msgstr "Tekst przycisków" #: apps/remix/app/components/general/template/template-type.tsx #: apps/remix/app/components/tables/templates-table.tsx @@ -8696,7 +8697,7 @@ msgstr "Aby podpisać pole, wymagane jest ponowne uwierzytelnianie" #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Reauthorise and retry" -msgstr "Ponownie autoryzuj i spróbuj ponownie" +msgstr "Spróbuj ponownie" #: packages/ui/components/recipient/recipient-role-select.tsx msgid "Receives copy" @@ -8741,11 +8742,11 @@ msgstr "Odbiorca zatwierdził dokument" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient authenticated with the signing provider" -msgstr "Odbiorca został uwierzytelniony u dostawcy podpisu" +msgstr "Odbiorca uwierzytelnił się u dostawcy podpisu" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient authorised the remote signature" -msgstr "Odbiorca autoryzował zdalny podpis" +msgstr "Odbiorca autoryzował podpis zdalny" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient CC'd the document" @@ -8795,7 +8796,7 @@ msgstr "Odbiorca poprosił o kod weryfikacyjny dla dokumentu" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient requested a remote signature" -msgstr "Odbiorca zażądał zdalnego podpisu" +msgstr "Odbiorca poprosił o podpis zdalny" #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Recipient signed" @@ -8811,7 +8812,7 @@ msgstr "Odbiorca podpisał dokument" #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Recipient signing request" -msgstr "Poproszono odbiorcę o podpisanie" +msgstr "Prośba o podpisanie wysłana do odbiorcy" #: packages/ui/components/document/document-email-checkboxes.tsx msgid "Recipient signing request email" @@ -8836,11 +8837,11 @@ msgstr "Odbiorca wyświetlił dokument" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient's remote signature was applied" -msgstr "Zastosowano zdalny podpis odbiorcy" +msgstr "Odbiorca złożył podpis zdalny" #: packages/lib/utils/document-audit-logs.ts msgid "Recipient's signing provider authentication failed" -msgstr "Uwierzytelnianie dostawcy usługi podpisu odbiorcy nie powiodło się" +msgstr "Uwierzytelnienie odbiorcy u dostawcy podpisu nie powiodło się" #: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx @@ -8868,7 +8869,7 @@ msgstr "Odbiorcy będą mogli podpisać dokument po jego wysłaniu" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "Recipients will be notified that the document was cancelled" -msgstr "Odbiorcy zostaną powiadomieni, że dokument został anulowany" +msgstr "Odbiorcy zostaną powiadomieni o anulowaniu dokumentu" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "Recipients will still retain their copy of the document" @@ -8963,7 +8964,7 @@ msgstr "Pamiętasz hasło? <0>Zaloguj się" #: packages/email/templates/document-reminder.tsx msgid "Reminder to {action} {documentName}" -msgstr "Sprawdź i {action} dokument {documentName}" +msgstr "Przypomnienie, aby {action} dokument „{documentName}”" #. placeholder {0}: envelope.documentMeta.subject #: packages/lib/jobs/definitions/internal/process-signing-reminder.handler.ts @@ -9107,7 +9108,7 @@ msgstr "Zgłoś nadawcę" #: apps/remix/app/routes/_recipient+/report.$token.tsx msgid "Report this sender?" -msgstr "Zgłosić tego nadawcę?" +msgstr "Zgłosić nadawcę?" #: apps/remix/app/components/general/organisation-usage-panel.tsx #: apps/remix/app/components/tables/admin-organisation-stats-table.tsx @@ -9329,7 +9330,7 @@ msgstr "Wyrównaj do lewej" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Ring" -msgstr "Obramowanie (ring)" +msgstr "Obramowanie zaznaczenia" #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -9420,7 +9421,7 @@ msgstr "Szukaj identyfikatora lub nazwy" #: apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx msgid "Search by document title, team:123 or user:123" -msgstr "Szukaj po tytule dokumentu, team:123 lub user:123" +msgstr "Szukaj tytułu dokumentu, zespołu lub użytkownika" #: apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx msgid "Search by domain or organisation name" @@ -9436,7 +9437,7 @@ msgstr "Szukaj nazwy lub adresu e-mail" #: apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx msgid "Search by name or from address" -msgstr "Szukaj po nazwie lub adresie nadawcy" +msgstr "Szukaj nazwy lub adresu e-mail" #: apps/remix/app/routes/_authenticated+/admin+/organisations._index.tsx msgid "Search by organisation ID, name, customer ID or owner email" @@ -9448,7 +9449,7 @@ msgstr "Szukaj nazwy organizacji" #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "Search by organisation name, URL or ID" -msgstr "Szukaj po nazwie organizacji, adresie URL lub ID" +msgstr "Szukaj nazwy organizacji, adresu URL lub identyfikatora" #: apps/remix/app/components/general/document/document-search.tsx msgid "Search documents..." @@ -9722,7 +9723,7 @@ msgstr "Wyślij" #: apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx msgid "Send a test email using this transport to verify the configuration." -msgstr "Wyślij wiadomość testową przy użyciu tego transportu, aby zweryfikować konfigurację." +msgstr "Wyślij wiadomość testową przy pomocy dostawcy poczty e-mail." #: apps/remix/app/components/dialogs/webhook-test-dialog.tsx msgid "Send a test webhook with sample data to verify your integration is working correctly." @@ -9781,11 +9782,11 @@ msgstr "Status wysyłki" #: apps/remix/app/components/tables/admin-email-transports-table.tsx msgid "Send test" -msgstr "Wyślij test" +msgstr "Wyślij wiadomość testową" #: apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx msgid "Send Test Email" -msgstr "Wyślij testową wiadomość e-mail" +msgstr "Wyślij wiadomość testową" #: apps/remix/app/components/tables/documents-table.tsx #: apps/remix/app/components/tables/inbox-table.tsx @@ -9838,7 +9839,7 @@ msgstr "Skonfiguruj właściwości szablonu i informacje o odbiorcach" #: packages/email/templates/admin-user-created.tsx msgid "Set your password for Documenso" -msgstr "Ustaw swoje hasło do Documenso" +msgstr "Ustaw hasło Documenso" #: apps/remix/app/components/general/app-command-menu.tsx #: apps/remix/app/components/general/app-command-menu.tsx @@ -9891,7 +9892,7 @@ msgstr "Pokaż ustawienia zaawansowane" #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "Show daily averages for documents, emails and API usages" -msgstr "Pokaż dzienne średnie dla dokumentów, e-maili i użycia API" +msgstr "Pokaż średnie dzienne wykorzystanie dokumentów, wiadomości i API" #: apps/remix/app/components/general/admin-license-card.tsx msgid "Show license key" @@ -9907,7 +9908,7 @@ msgstr "Pokaż użycie" #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "Show usage with quotas" -msgstr "Pokaż użycie z limitami" +msgstr "Pokaż limity użycia" #: apps/remix/app/components/dialogs/sign-field-signature-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-2fa.tsx @@ -9959,7 +9960,7 @@ msgstr "Podpisz dokument" #: apps/remix/app/routes/_recipient+/_layout.tsx msgid "Sign Document - Documenso" -msgstr "Podpisz dokument – Documenso" +msgstr "Podpisz dokument - Documenso" #: apps/remix/app/components/embed/multisign/multi-sign-document-list.tsx msgid "Sign Documents" @@ -10115,7 +10116,7 @@ msgstr "Podpisuje" #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "Signing algorithm is not supported" -msgstr "Algorytm podpisu nie jest obsługiwany" +msgstr "Algorytm podpisywania nie jest obsługiwany" #: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx #: packages/lib/server-only/pdf/render-certificate.ts @@ -10142,7 +10143,7 @@ msgstr "Czas na podpisanie minął" #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Signing failed" -msgstr "Podpisanie nie powiodło się" +msgstr "Podpisywanie nie powiodło się" #: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx msgid "Signing for" @@ -10273,7 +10274,7 @@ msgstr "Coś poszło nie tak" #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Something went wrong while applying your signature. Please retry." -msgstr "Coś poszło nie tak podczas stosowania Twojego podpisu. Spróbuj ponownie." +msgstr "Coś poszło nie tak podczas składania podpisu. Spróbuj ponownie." #. placeholder {0}: data.teamName #: apps/remix/app/routes/_unauthenticated+/team.verify.email.$token.tsx @@ -10298,11 +10299,11 @@ msgstr "Coś poszło nie tak podczas ładowania kluczy dostępu." #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "Something went wrong while preparing the remote signature. Please try again." -msgstr "Coś poszło nie tak podczas przygotowywania zdalnego podpisu. Spróbuj ponownie." +msgstr "Coś poszło nie tak podczas przygotowywania podpisu zdalnego. Spróbuj ponownie." #: packages/lib/constants/pdf-viewer-i18n.ts msgid "Something went wrong while rendering the document, please try again or contact our support." -msgstr "Coś poszło nie tak podczas renderowania dokumentu. Spróbuj ponownie lub skontaktuj się z naszym wsparciem." +msgstr "Coś poszło nie tak podczas renderowania dokumentu. Spróbuj ponownie lub skontaktuj się z pomocą techniczną." #: packages/lib/constants/pdf-viewer-i18n.ts #: packages/lib/constants/pdf-viewer-i18n.ts @@ -10589,7 +10590,7 @@ msgstr "Synchronizuj" #: apps/remix/app/components/dialogs/admin-organisation-sync-subscription-dialog.tsx msgid "Sync claims. This will overwrite the current claim with the one resolved from the Stripe subscription." -msgstr "Zsynchronizuj roszczenia. Spowoduje to nadpisanie bieżącego roszczenia tym, które zostanie pobrane z subskrypcji Stripe." +msgstr "Synchronizuj subskrypcję. Spowoduje to nadpisanie ustawień Stripe." #: apps/remix/app/components/tables/organisation-email-domains-table.tsx msgid "Sync Email Domains" @@ -10607,7 +10608,7 @@ msgstr "Synchronizuj licencję z serwera" #: apps/remix/app/components/dialogs/admin-organisation-sync-subscription-dialog.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Sync Stripe subscription" -msgstr "Zsynchronizuj subskrypcję Stripe" +msgstr "Synchronizuj subskrypcję Stripe" #: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts @@ -10873,7 +10874,7 @@ msgstr "Zmieniono nazwę szablonu" #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx msgid "Template resent" -msgstr "Szablon został ponownie wysłany" +msgstr "Wysłano ponownie szablon" #: apps/remix/app/components/general/template/template-edit-form.tsx msgid "Template saved" @@ -10934,7 +10935,7 @@ msgstr "Wiadomość testowa została wysłana." #: apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx msgid "Test failed." -msgstr "Test nie powiódł się." +msgstr "Wiadomość testowa nie powiodła się." #: apps/remix/app/components/dialogs/webhook-test-dialog.tsx msgid "Test Webhook" @@ -10950,7 +10951,7 @@ msgstr "Testowy webhook został wysłany" #: apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx msgid "test@example.com" -msgstr "test@example.com" +msgstr "test@przyklad.com" #: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx #: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx @@ -10979,7 +10980,7 @@ msgstr "Kolor tekstu" #: apps/remix/app/components/forms/branding-preferences-form.tsx msgid "Text colour on primary buttons." -msgstr "Kolor tekstu na głównych przyciskach." +msgstr "Kolor tekstu na przyciskach." #: apps/remix/app/components/dialogs/sign-field-text-dialog.tsx msgid "Text is required" @@ -10995,7 +10996,7 @@ msgstr "Podpisywanie zostało zakończone." #: apps/remix/app/routes/_recipient+/report.$token.tsx msgid "Thank you for letting us know, we have flagged this sender for review. If you have any concerns please feel free to reach out to our <0>support team." -msgstr "Dziękujemy za zgłoszenie — oznaczyliśmy tego nadawcę do sprawdzenia. Jeśli masz jakiekolwiek wątpliwości, skontaktuj się z naszym <0>zespołem wsparcia." +msgstr "Dziękujemy za zgłoszenie nadawcy. Zweryfikujemy go. Jeśli masz pytania, skontaktuj się z naszą <0>pomocą techniczną." #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx msgid "Thank you for using Documenso 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." @@ -11039,7 +11040,7 @@ msgstr "Domyślny adres e-mail do wysyłania wiadomości do odbiorców" #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "The deletion will run in the background, and can take up to a few minutes to complete. Do not re-run this deletion." -msgstr "Usunięcie zostanie wykonane w tle i może zająć do kilku minut. Nie uruchamiaj ponownie tego procesu usuwania." +msgstr "Usunięcie zostanie uruchomione w tle. Może to potrwać do kilku minut. Nie uruchamiaj ponownie operacji." #: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx #: apps/remix/app/components/general/template/template-direct-link-badge.tsx @@ -11054,7 +11055,7 @@ msgstr "Nazwa wyświetlana dla adresu e-mail" #: apps/remix/app/utils/toast-error-messages.ts msgid "The document could not be created because of missing or invalid information. Please review the template's recipients and fields." -msgstr "Nie udało się utworzyć dokumentu z powodu brakujących lub nieprawidłowych danych. Sprawdź listę odbiorców i pola w szablonie." +msgstr "Nie można było utworzyć dokumentu z powodu brakujących lub nieprawidłowych informacji. Sprawdź odbiorców i pola szablonu." #: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx msgid "The Document has been deleted successfully." @@ -11091,7 +11092,7 @@ msgstr "Zmieniono właściciela dokumentu na {0} z zespołu {1}" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "The document signing process will be stopped" -msgstr "Proces podpisywania dokumentu zostanie zatrzymany" +msgstr "Podpisywanie dokumentu zostanie zatrzymane" #: apps/remix/app/utils/toast-error-messages.ts msgid "The document was created but could not be sent to recipients." @@ -11124,7 +11125,7 @@ msgstr "Dokument zostanie natychmiast wysłany do odbiorców." #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx msgid "The document will remain in your dashboard marked as Cancelled" -msgstr "Dokument pozostanie na Twoim pulpicie oznaczony jako „Anulowany”." +msgstr "Dokument pozostanie na pulpicie oznaczony jako anulowany" #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._layout.tsx msgid "The document you are looking for could not be found." @@ -11141,7 +11142,7 @@ msgstr "Nazwa dokumentu" #: apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx msgid "The documents will remain in your dashboard marked as Cancelled" -msgstr "Dokumenty pozostaną na Twoim pulpicie oznaczone jako „Anulowane”." +msgstr "Dokumenty pozostaną na pulpicie oznaczone jako anulowane" #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "The email address which will show up in the \"Reply To\" field in emails" @@ -11149,7 +11150,7 @@ msgstr "Adres e-mail, który pojawi się w polu „Odpowiedz do” w wiadomości #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "The email blocklist has been updated successfully." -msgstr "Bloklista e‑mail została pomyślnie zaktualizowana." +msgstr "Lista zablokowanych domen została zaktualizowana." #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains.$id.tsx msgid "The email domain you are looking for may have been removed, renamed or may have never existed." @@ -11190,11 +11191,11 @@ msgstr "Wystąpiły następujące błędy:" #: packages/email/templates/organisation-delete.tsx msgid "The following organisation has been deleted by an administrator. You and your members will no longer be able to access this organisation, its teams, or its associated data." -msgstr "Poniższa organizacja została usunięta przez administratora. Ty i członkowie Twojej organizacji nie będziecie już mieli dostępu do tej organizacji, jej zespołów ani powiązanych z nią danych." +msgstr "Organizacja została usunięta przez administratora. Użytkownicy organizacji nie będą mieli już do niej dostępu, w tym do jej zespołów i powiązanych danych." #: packages/email/templates/organisation-delete.tsx msgid "The following organisation has been deleted. You and your members will no longer be able to access this organisation, its teams, or its associated data." -msgstr "Poniższa organizacja została usunięta. Ty i członkowie Twojej organizacji nie będziecie już mieli dostępu do tej organizacji, jej zespołów ani powiązanych z nią danych." +msgstr "Organizacja została usunięta. Użytkownicy organizacji nie będą mieli już do niej dostępu, w tym do jej zespołów i powiązanych danych." #: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx msgid "The following recipients require an email address:" @@ -11240,7 +11241,7 @@ msgstr "Subskrypcja organizacji została zsynchronizowana ze Stripe." #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "The organisation will be deleted in the background. Documents will be orphaned, not deleted." -msgstr "Organizacja zostanie usunięta w tle. Dokumenty staną się osierocone, nieusunięte." +msgstr "Organizacja zostanie usunięta w tle. Dokumenty zostaną osierocone, ale nie usunięte." #: apps/remix/app/routes/_authenticated+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -11337,7 +11338,7 @@ msgstr "Link do podpisywania został skopiowany do schowka." #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "The signing provider did not respond in time. Please retry." -msgstr "Dostawca usługi podpisu nie odpowiedział na czas. Spróbuj ponownie." +msgstr "Dostawca podpisu nie odpowiedział na czas. Spróbuj ponownie." #: packages/email/templates/recipient-expired.tsx msgid "The signing window for \"{recipientName}\" on document \"{documentName}\" has expired." @@ -11364,7 +11365,7 @@ msgstr "Zespół, którego szukasz, mógł zostać usunięty, zmieniony lub móg #: apps/remix/app/utils/toast-error-messages.ts msgid "The template or one of its recipients could not be found." -msgstr "Szablon lub jeden z jego odbiorców nie został znaleziony." +msgstr "Nie można odnaleźć szablonu lub jednego z jego odbiorców." #: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx msgid "The template will be removed from your profile" @@ -11453,7 +11454,7 @@ msgstr "Brak aktywnych szkiców. Prześlij, aby utworzyć." #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "There are no cancelled documents. Documents you cancel will remain here as a record that they were distributed." -msgstr "Brak anulowanych dokumentów. Dokumenty, które anulujesz, pozostaną tutaj jako zapis ich wysłania." +msgstr "Brak anulowanych dokumentów. Anulowane dokumenty pozostaną tutaj jako dowód ich wcześniejszej dystrybucji." #: apps/remix/app/components/tables/documents-table-empty-state.tsx msgid "There are no completed documents yet. Documents that you have created or received will appear here once completed." @@ -11522,7 +11523,7 @@ msgstr "Nie można zmienić dokumentu" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx msgid "This document could not be cancelled at this time. Please try again." -msgstr "Tego dokumentu nie można teraz anulować. Spróbuj ponownie." +msgstr "Nie można anulować dokumentu. Spróbuj ponownie." #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "This document could not be deleted at this time. Please try again." @@ -11547,7 +11548,7 @@ msgstr "Dokument został już wysłany do odbiorcy, więc nie możesz go już ed #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx msgid "This document has been cancelled" -msgstr "Ten dokument został anulowany" +msgstr "Dokument został anulowany" #: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx msgid "This document has been cancelled by the owner and is no longer available for others to sign." @@ -11568,7 +11569,7 @@ msgstr "Dokument został podpisany przez wszystkich odbiorców" #: apps/remix/app/utils/toast-error-messages.ts msgid "This document has too many recipients. Please remove some recipients or contact support if you need more." -msgstr "Ten dokument ma zbyt wielu adresatów. Usuń część adresatów lub skontaktuj się z pomocą techniczną, jeśli potrzebujesz większej liczby." +msgstr "Dokument ma zbyt wielu odbiorców. Usuń niektórych odbiorców lub skontaktuj się z pomocą techniczną, jeśli potrzebujesz więcej." #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx msgid "This document is available in your Documenso account. You can view more details, recipients, and audit logs there." @@ -11629,11 +11630,11 @@ msgstr "Zostanie wysłana do odbiorcy, który właśnie podpisał dokument, gdy #: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx #: packages/ui/primitives/document-flow/add-signers.tsx msgid "This envelope cannot have more than {recipientCountLimit} recipients. Please contact support if you need more." -msgstr "Ta koperta nie może mieć więcej niż {recipientCountLimit} adresatów. Skontaktuj się z pomocą techniczną, jeśli potrzebujesz większej liczby." +msgstr "Koperta nie może więcej niż {recipientCountLimit} odbiorców. Skontaktuj się z pomocą techniczną, jeśli potrzebujesz więcej." #: apps/remix/app/components/embed/embed-paywall.tsx msgid "This feature is not available on your current plan" -msgstr "Funkcja nie jest dostępna w Twoim planie" +msgstr "Ta funkcja nie jest dostępna w Twoim obecnym planie." #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them." @@ -11641,7 +11642,7 @@ msgstr "Pole nie może być modyfikowane ani usuwane. Gdy udostępnisz bezpośre #: apps/remix/app/utils/toast-error-messages.ts msgid "This file type isn't supported. Please upload a PDF or Word document." -msgstr "Ten typ pliku nie jest obsługiwany. Prześlij plik PDF lub dokument programu Word." +msgstr "Typ pliku nie jest obsługiwany. Prześlij dokument PDF lub Word." #: apps/remix/app/components/dialogs/folder-delete-dialog.tsx msgid "This folder contains multiple items. Deleting it will remove all subfolders and move all nested documents and templates to the root folder." @@ -11674,11 +11675,11 @@ msgstr "Link jest nieprawidłowy lub wygasł. Skontaktuj się ze swoim zespołem #: apps/remix/app/components/dialogs/team-member-delete-dialog.tsx msgid "This member is inherited from a group and cannot be removed from the team directly." -msgstr "Ten członek jest dziedziczony z grupy i nie można go bezpośrednio usunąć z zespołu." +msgstr "Użytkownik jest odziedziczony z grupy i nie może zostać usunięty bezpośrednio z zespołu." #: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx msgid "This organisation is awaiting payment. Complete checkout to unlock it." -msgstr "Ta organizacja oczekuje na płatność. Dokończ proces zakupu, aby ją odblokować." +msgstr "Organizacja oczekuje na płatność. Zakończ płatność, aby ją odblokować." #: apps/remix/app/routes/_unauthenticated+/organisation.sso.confirmation.$token.tsx msgid "This organisation will have administrative control over your account. You can revoke this access later, but they will retain access to any data they've already collected." @@ -11758,7 +11759,7 @@ msgstr "Zostanie wysłana do właściciela po zakończeniu dokumentu." #: packages/ui/components/document/document-email-checkboxes.tsx msgid "This will be sent to the document owner when a recipient's signing window has expired." -msgstr "Zostanie wysłana do właściciela dokumentu, gdy minie czas na podpisanie przez odbiorcę\n" +msgstr "Zostanie wysłana do właściciela dokumentu, gdy minie czas na podpisanie przez odbiorcę" #: apps/remix/app/components/tables/organisation-email-domains-table.tsx msgid "This will check and sync the status of all email domains for this organisation" @@ -11902,7 +11903,7 @@ msgstr "Aby uzyskać dostęp do konta, potwierdź adres e-mail, klikając na lin #: packages/email/template-components/template-admin-user-created.tsx msgid "To get started, please set your password by clicking the button below:" -msgstr "Aby rozpocząć, ustaw swoje hasło, klikając przycisk poniżej:" +msgstr "Ustaw hasło, klikając przycisk poniżej:" #. placeholder {0}: recipient.email #: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx @@ -12011,7 +12012,7 @@ msgstr "Token nie został znaleziony" #: apps/remix/app/utils/toast-error-messages.ts msgid "Too many recipients" -msgstr "Zbyt wielu adresatów" +msgstr "Zbyt wielu odbiorców" #: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx #: apps/remix/app/components/dialogs/ai-recipient-detection-dialog.tsx @@ -12024,7 +12025,7 @@ msgstr "Góra" #: apps/remix/app/components/tables/admin-organisation-stats-table.tsx msgid "Total" -msgstr "Razem" +msgstr "Łącznie" #: apps/remix/app/routes/_authenticated+/admin+/stats.tsx msgid "Total Documents" @@ -12052,23 +12053,23 @@ msgstr "Przenieś dokumenty do innego zespołu" #: apps/remix/app/components/dialogs/email-transport-create-dialog.tsx msgid "Transport created." -msgstr "Transport został utworzony." +msgstr "Dostawca poczty e-mail został utworzony." #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "Transport deleted." -msgstr "Transport został usunięty." +msgstr "Dostawca poczty e-mail został usunięty." #: apps/remix/app/components/forms/email-transport-form.tsx msgid "Transport type" -msgstr "Typ transportu" +msgstr "Rodzaj dostawcy poczty e-mail" #: apps/remix/app/components/forms/email-transport-form.tsx msgid "Transport type cannot be changed after creation." -msgstr "Typu transportu nie można zmienić po utworzeniu." +msgstr "Nie można zmienić rodzaju dostawcy poczty e-mail po jego utworzeniu." #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx msgid "Transport updated." -msgstr "Transport został zaktualizowany." +msgstr "Dostawca poczty e-mail został zaktualizowany." #: apps/remix/app/components/dialogs/webhook-create-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx @@ -12283,7 +12284,7 @@ msgstr "Nieznana nazwa" #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "Unlimited" -msgstr "Nieograniczone" +msgstr "Bez ograniczeń" #: apps/remix/app/components/dialogs/organisation-create-dialog.tsx msgid "Unlimited documents, API and more" @@ -12347,7 +12348,7 @@ msgstr "Zaktualizuj płatności" #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "Update Blocklist" -msgstr "Zaktualizuj listę blokowanych adresów e-mail" +msgstr "Zaktualizuj listę zablokowanych domen" #: apps/remix/app/components/dialogs/claim-update-dialog.tsx msgid "Update Claim" @@ -12583,7 +12584,7 @@ msgstr "Adres URL" #. placeholder {0}: selectedStat?.period || 'N/A' #: apps/remix/app/components/general/organisation-usage-panel.tsx msgid "Usage for period: {0}" -msgstr "Wykorzystanie za okres: {0}" +msgstr "Okres: {0}" #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx msgid "Use" @@ -12614,7 +12615,7 @@ msgstr "Użyj klucza dostępu do uwierzytelniania" #: apps/remix/app/components/tables/admin-email-transports-table.tsx msgid "Used by claims" -msgstr "Używany przez roszczenia" +msgstr "Liczba powiązań" #: apps/remix/app/components/tables/admin-document-logs-table.tsx #: apps/remix/app/components/tables/document-logs-table.tsx @@ -12630,7 +12631,7 @@ msgstr "User agent" #: apps/remix/app/components/dialogs/admin-user-create-dialog.tsx msgid "User created and welcome email sent" -msgstr "Użytkownik został utworzony i wysłano wiadomość powitalną" +msgstr "Użytkownik został utworzony. Wiadomość powitalna została wysłana." #: apps/remix/app/components/forms/password.tsx msgid "User has no password." @@ -12897,7 +12898,7 @@ msgstr "Wyświetl rekordy DNS dla tej domeny" #: apps/remix/app/routes/_authenticated+/admin+/organisation-stats._index.tsx msgid "View, sort and filter monthly usage stats across organisations" -msgstr "Przeglądaj, sortuj i filtruj statystyki miesięcznego wykorzystania w organizacjach" +msgstr "Sprawdź miesięczne statystyki wykorzystania organizacji" #: apps/remix/app/components/embed/multisign/multi-sign-document-list.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx @@ -12944,7 +12945,7 @@ msgstr "Oczekiwanie na innych" #: packages/lib/server-only/document/send-pending-email.ts msgid "Waiting for others to complete signing." -msgstr "Oczekiwanie na innych, aby zakończyć podpisywanie." +msgstr "Oczekiwanie na zakończenie podpisywania przez innych." #: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx msgid "Waiting for others to sign" @@ -12966,7 +12967,7 @@ msgstr "Chcesz mieć profil publiczny?" #: apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx msgid "Warning, this email transport is currently being used by:" -msgstr "Uwaga, ten transport e-mail jest obecnie używany przez:" +msgstr "Ten dostawca poczty e-mail jest używany przez:" #: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -12995,7 +12996,7 @@ msgstr "Nie mogliśmy zaktualizować klucza dostępu. Spróbuj ponownie późnie #: apps/remix/app/utils/toast-error-messages.ts msgid "We couldn't convert this file. Please check it's a valid Word document or upload a PDF instead." -msgstr "Nie udało się przekonwertować tego pliku. Sprawdź, czy jest to prawidłowy dokument programu Word, lub zamiast tego prześlij plik PDF." +msgstr "Nie możemy przekonwertować tego pliku. Sprawdź, czy plik Word jest prawidłowy lub prześlij plik PDF." #: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx msgid "We couldn't create a Stripe customer. Please try again." @@ -13025,7 +13026,7 @@ msgstr "Nie mogliśmy zaktualizować dostawcy. Spróbuj ponownie." #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "We encountered an error while attempting to delete this organisation. Please try again later." -msgstr "Wystąpił błąd podczas próby usunięcia tej organizacji. Spróbuj ponownie później." +msgstr "Wystąpił błąd podczas próby usunięcia organizacji. Spróbuj ponownie później." #: packages/lib/client-only/providers/envelope-editor-provider.tsx #: packages/lib/client-only/providers/envelope-editor-provider.tsx @@ -13177,7 +13178,7 @@ msgstr "Wystąpił nieznany błąd podczas próby zaktualizowania baneru. Sprób #: apps/remix/app/components/general/admin-email-blocklist-section.tsx msgid "We encountered an unknown error while attempting to update the email blocklist. Please try again later." -msgstr "Wystąpił nieznany błąd podczas próby zaktualizowania listy blokowanych adresów e-mail. Spróbuj ponownie później." +msgstr "Wystąpił nieznany błąd podczas próby zaktualizowania listy zablokowanych domen. Spróbuj ponownie później." #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx msgid "We encountered an unknown error while attempting to update the envelope. Please try again later." @@ -13258,7 +13259,7 @@ msgstr "Nie mogliśmy Cię wylogować." #: apps/remix/app/routes/_recipient+/report.$token.tsx msgid "We were unable to report this sender at this time. Please try again later." -msgstr "Nie mogliśmy zgłosić tego nadawcy w tym momencie. Spróbuj ponownie później." +msgstr "Nie mogliśmy zgłosić nadawcę. Spróbuj ponownie później." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx msgid "We were unable to set your public profile to public. Please try again." @@ -13344,7 +13345,7 @@ msgstr "Pusto" #: packages/email/template-components/template-document-pending.tsx msgid "We're still waiting for other signers to sign this document.<0/>We'll notify you as soon as it's ready." -msgstr "Wciąż czekamy na podpisy pozostałych podpisujących.<0/>Powiadomimy Cię, gdy dokument będzie gotowy." +msgstr "Wciąż czekamy na podpisy pozostałych osób.<0/>Powiadomimy Cię, gdy dokument będzie gotowy." #: packages/email/templates/reset-password.tsx msgid "We've changed your password as you asked. You can now sign in with your new password." @@ -13352,15 +13353,15 @@ msgstr "Zmieniliśmy Twoje hasło. Możesz zalogować się za pomocą nowego has #: packages/email/templates/organisation-limit-alert.tsx msgid "We've noticed API activity on your account that exceeds the fair use limits of your current plan. As a precaution, new API activity has been temporarily paused pending review." -msgstr "Zauważyliśmy aktywność API na Twoim koncie, która przekracza limity uczciwego korzystania w Twoim obecnym planie. W ramach środka ostrożności nowa aktywność API została tymczasowo wstrzymana do czasu weryfikacji." +msgstr "Zauważyliśmy na Twoim koncie aktywność API, która przekracza limity dozwolonego użytkowania w ramach obecnego planu. Nowa aktywność API została tymczasowo wstrzymana do czasu przeprowadzenia weryfikacji." #: packages/email/templates/organisation-limit-alert.tsx msgid "We've noticed document activity on your account that exceeds the fair use limits of your current plan. As a precaution, new document activity has been temporarily paused pending review." -msgstr "Zauważyliśmy aktywność związaną z dokumentami na Twoim koncie, która przekracza limity uczciwego korzystania w Twoim obecnym planie. W ramach środka ostrożności nowa aktywność związana z dokumentami została tymczasowo wstrzymana do czasu weryfikacji." +msgstr "Zauważyliśmy na Twoim koncie aktywność dokumentów, która przekracza limity dozwolonego użytkowania w ramach obecnego planu. Nowa aktywność została tymczasowo wstrzymana do czasu przeprowadzenia weryfikacji." #: packages/email/templates/organisation-limit-alert.tsx msgid "We've noticed email sending activity on your account that exceeds the fair use limits of your current plan. As a precaution, new email activity has been temporarily paused pending review." -msgstr "Zauważyliśmy wysyłkę e‑maili z Twojego konta, która przekracza limity uczciwego korzystania w Twoim obecnym planie. W ramach środka ostrożności nowa aktywność e‑mailowa została tymczasowo wstrzymana do czasu weryfikacji." +msgstr "Zauważyliśmy na Twoim koncie aktywność wiadomości, która przekracza limity dozwolonego użytkowania w ramach obecnego planu. Nowa aktywność została tymczasowo wstrzymana do czasu przeprowadzenia weryfikacji." #: apps/remix/app/components/general/document-signing/access-auth-2fa-form.tsx msgid "We've sent a 6-digit verification code to your email. Please enter it below to complete the document." @@ -13459,7 +13460,7 @@ msgstr "Podpisujący mogą wybrać, kto powinien podpisać jako następny w kole #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "When enabled, users signing in via SSO for the first time will also receive their own personal organisation." -msgstr "Użytkownicy logujący się po raz pierwszy za pomocą logowania SSO otrzymają również własną osobistą organizację." +msgstr "Po włączeniu tej opcji użytkownicy logujący się po raz pierwszy za pomocą SSO otrzymają również własną osobistą organizację." #: apps/remix/app/components/dialogs/passkey-create-dialog.tsx msgid "When you click continue, you will be prompted to add the first available authenticator on your system." @@ -13531,7 +13532,7 @@ msgstr "Zatwierdziłeś dokument" #: apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx msgid "You are about to cancel <0>\"{title}\"" -msgstr "Zamierzasz anulować dokument <0>\"{title}\"" +msgstr "Zamierzasz anulować dokument <0>„{title}”" #: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx msgid "You are about to complete approving the following document" @@ -13560,7 +13561,7 @@ msgstr "Zamierzasz usunąć organizację <0>{0}. Wszystkie dane powiązane z #: apps/remix/app/components/dialogs/admin-organisation-delete-dialog.tsx msgid "You are about to delete <0>{organisationName}. This action is not reversible. All teams will be removed and all documents will be orphaned to the deleted-account service account." -msgstr "Za chwilę usuniesz <0>{organisationName}. Tej operacji nie można cofnąć. Wszystkie zespoły zostaną usunięte, a wszystkie dokumenty zostaną przypisane do konta usługi usuniętego konta." +msgstr "Zamierzasz usunąć organizację <0>{organisationName}. Ta akcja jest nieodwracalna. Wszystkie zespoły zostaną usunięcie, a dokumenty zostaną przypisane do konta serwisowego usuniętego konta." #: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx msgid "You are about to delete the following team email from <0>{teamName}." @@ -13710,11 +13711,11 @@ msgstr "Nie masz uprawnień do zresetowania weryfikacji dwuetapowej tego użytko #: packages/lib/utils/document-audit-logs.ts msgid "You authenticated with the signing provider" -msgstr "Uwierzytelniłeś(-aś) się u dostawcy usługi podpisu" +msgstr "Uwierzytelniłeś się u dostawcy podpisu" #: packages/lib/utils/document-audit-logs.ts msgid "You authorised the remote signature" -msgstr "Autoryzowałeś(-aś) zdalny podpis" +msgstr "Autoryzowałeś podpis zdalny" #: apps/remix/app/components/dialogs/ai-field-detection-dialog.tsx msgid "You can add fields manually in the editor." @@ -13730,7 +13731,7 @@ msgstr "Możesz także skopiować i wkleić link do przeglądarki: {confirmation #: packages/email/template-components/template-admin-user-created.tsx msgid "You can also copy and paste this link into your browser: {resetPasswordLink} (link expires in 24 hours)" -msgstr "Możesz także skopiować i wkleić ten link do przeglądarki: {resetPasswordLink} (link wygaśnie za 24 godziny)" +msgstr "Możesz także skopiować i wkleić link do przeglądarki: {resetPasswordLink} (link wygaśnie za 24 godziny)" #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx msgid "You can choose to enable or disable the profile for public view." @@ -13809,11 +13810,11 @@ msgstr "Nie możesz edytować użytkownika zespołu, który ma wyższą rolę ni #: apps/remix/app/components/dialogs/team-member-delete-dialog.tsx msgid "You cannot remove a member with a role higher than your own." -msgstr "Nie możesz usunąć członka, który ma wyższą rolę niż Ty." +msgstr "Nie możesz usunąć użytkownika o roli wyższej niż Twoja." #: apps/remix/app/components/dialogs/team-member-delete-dialog.tsx msgid "You cannot remove members from this team while the inherit member feature is enabled." -msgstr "Nie możesz usuwać członków z tego zespołu, gdy funkcja dziedziczenia członków jest włączona." +msgstr "Nie możesz usunąć użytkowników zespołu, jeśli funkcja odziedziczenia użytkownika jest włączona." #: apps/remix/app/components/dialogs/team-member-delete-dialog.tsx msgid "You cannot remove the organisation owner from the team." @@ -13934,7 +13935,7 @@ msgstr "Odrzucono zaproszenie dołączenia do organizacji <0>{0}." #: packages/lib/jobs/definitions/emails/send-signing-email.handler.ts #: packages/lib/server-only/document/resend-document.ts msgid "You have initiated the document {0} that requires you to {recipientActionVerb} it." -msgstr "Sprawdź i {recipientActionVerb} dokument utworzony przez Ciebie." +msgstr "Sprawdź i {recipientActionVerb} dokument „{0}” utworzony przez Ciebie." #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx msgid "You have no webhooks yet. Your webhooks will be shown here once you create them." @@ -13967,7 +13968,7 @@ msgstr "Osiągnięto maksymalną miesięczną liczbę dokumentów. Zaktualizuj p #: apps/remix/app/utils/toast-error-messages.ts msgid "You have reached your document limit for this plan. Please upgrade your plan." -msgstr "Osiągnięto limit liczby dokumentów w tym planie. Zaktualizuj swój plan." +msgstr "Osiągnięto maksymalną liczbę dokumentów. Zaktualizuj plan." #: apps/remix/app/components/general/document/document-upload-button-legacy.tsx #: apps/remix/app/components/general/envelope/envelope-upload-button.tsx @@ -14201,7 +14202,7 @@ msgstr "Poprosiłeś o kod weryfikacyjny dla dokumentu" #: packages/lib/utils/document-audit-logs.ts msgid "You requested a remote signature" -msgstr "Zleciłeś(-aś) wykonanie zdalnego podpisu" +msgstr "Poprosiłeś o podpis zdalny" #. placeholder {0}: data.recipientEmail #: packages/lib/utils/document-audit-logs.ts @@ -14414,7 +14415,7 @@ msgstr "Dokument został usunięty przez administratora!" #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx msgid "Your document has been resent successfully." -msgstr "Twój dokument został pomyślnie ponownie wysłany." +msgstr "Dokument został ponownie wysłany." #: apps/remix/app/components/dialogs/envelope-save-as-template-dialog.tsx msgid "Your document has been saved as a template." @@ -14511,7 +14512,7 @@ msgstr "Organizacja została utworzona." #: packages/email/templates/organisation-delete.tsx #: packages/email/templates/organisation-delete.tsx msgid "Your organisation has been deleted" -msgstr "Twoja organizacja została usunięta" +msgstr "Organizacja została usunięta" #: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx msgid "Your organisation has been successfully deleted." @@ -14523,47 +14524,47 @@ msgstr "Organizacja została zaktualizowana." #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Your organisation has exceeded a fair use limit" -msgstr "Twoja organizacja przekroczyła limit uczciwego użytkowania." +msgstr "Organizacja przekroczyła limit dozwolonego użytkowania" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Your organisation has exceeded a fair use limit. Please contact <0>support to review your plan's limits." -msgstr "Twoja organizacja przekroczyła limit uczciwego użytkowania. Skontaktuj się z <0>pomocą techniczną, aby przejrzeć limity swojego planu." +msgstr "Organizacja przekroczyła limit dozwolonego użytkowania. Skontaktuj się z <0>pomocą techniczną, aby przeprowadzić weryfikację." #: apps/remix/app/utils/toast-error-messages.ts msgid "Your organisation has reached its plan's fair use limit. Please contact your organisation administrator or support to continue." -msgstr "Twoja organizacja osiągnęła limit uczciwego użytkowania w swoim planie. Skontaktuj się z administratorem organizacji lub pomocą techniczną, aby kontynuować." +msgstr "Organizacja przekroczyła limit dozwolonego użytkowania. Skontaktuj się z administratorem organizacji." #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Your organisation is approaching a fair use limit" -msgstr "Twoja organizacja zbliża się do limitu dozwolonego użytkowania (fair use)" +msgstr "Organizacja zbliża się do limitu dozwolonego użytkowania" #: apps/remix/app/components/general/organisations/organisation-quota-banner.tsx msgid "Your organisation is approaching a fair use limit. If you expect to need higher limits, please contact <0>support to review your plan's limits." -msgstr "Twoja organizacja zbliża się do limitu dozwolonego użytkowania (fair use). Jeśli spodziewasz się potrzeby wyższych limitów, skontaktuj się z <0>pomocą techniczną, aby omówić limity w Twoim planie." +msgstr "Organizacja zbliża się do limitu dozwolonego użytkowania. Jeśli potrzebujesz wyższych limitów, skontaktuj się z <0>pomocą techniczną." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is generating API requests faster than normal, so some requests are being temporarily throttled." -msgstr "Twoja organizacja generuje żądania API szybciej niż zwykle, więc część żądań jest tymczasowo ograniczana (throttling)." +msgstr "Twoja organizacja generuje żądania API szybciej niż zwykle, więc niektóre żądania zostały tymczasowo ograniczane." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is generating documents faster than normal, so some requests are being temporarily throttled." -msgstr "Twoja organizacja generuje dokumenty szybciej niż zwykle, więc część żądań jest tymczasowo ograniczana (throttling)." +msgstr "Twoja organizacja generuje dokumenty szybciej niż zwykle, więc niektóre żądania zostały tymczasowo ograniczane." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is generating emails faster than normal, so some requests are being temporarily throttled." -msgstr "Twoja organizacja generuje e‑maile szybciej niż zwykle, więc część żądań jest tymczasowo ograniczana (throttling)." +msgstr "Twoja organizacja generuje wiadomości szybciej niż zwykle, więc niektóre żądania zostały tymczasowo ograniczane." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is nearing its fair use limits for creating documents on your current plan. Once the limit is reached, new document activity will be temporarily paused." -msgstr "Twoja organizacja zbliża się do limitów dozwolonego użytkowania (fair use) w zakresie tworzenia dokumentów w ramach obecnego planu. Po osiągnięciu limitu nowe działania związane z dokumentami zostaną tymczasowo wstrzymane." +msgstr "Organizacja zbliża się do limitu dozwolonego użytkowania dokumentów. Po przekroczeniu limitu tworzenie nowych dokumentów zostanie wstrzymane." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is nearing its fair use limits for making API requests on your current plan. Once the limit is reached, new API activity will be temporarily paused." -msgstr "Twoja organizacja zbliża się do limitów dozwolonego użytkowania (fair use) w zakresie wykonywania zapytań API w ramach obecnego planu. Po osiągnięciu limitu nowa aktywność API zostanie tymczasowo wstrzymana." +msgstr "Organizacja zbliża się do limitu dozwolonego użytkowania API. Po przekroczeniu limitu nowe żądania API zostaną wstrzymane." #: packages/email/templates/organisation-limit-alert.tsx msgid "Your organisation is nearing its fair use limits for sending email on your current plan. Once the limit is reached, new email activity will be temporarily paused." -msgstr "Twoja organizacja zbliża się do limitów dozwolonego użytkowania (fair use) w zakresie wysyłania e‑maili w ramach obecnego planu. Po osiągnięciu limitu nowa aktywność e‑mail zostanie tymczasowo wstrzymana." +msgstr "Organizacja zbliża się do limitu dozwolonego użytkowania wiadomości. Po przekroczeniu limitu tworzenie nowych wiadomości zostanie wstrzymane." #: apps/remix/app/components/forms/password.tsx #: apps/remix/app/components/forms/reset-password.tsx @@ -14613,27 +14614,27 @@ msgstr "To są Twoje kody odzyskiwania. Przechowuj je w bezpiecznym miejscu." #: packages/lib/utils/document-audit-logs.ts msgid "Your remote signature was applied" -msgstr "Twój zdalny podpis został zastosowany" +msgstr "Złożyłeś podpis zdalny" #: apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx msgid "Your signing authorisation expired before the signature could be applied. Please reauthorise to retry." -msgstr "Twoje upoważnienie do podpisu wygasło, zanim podpis mógł zostać złożony. Aby spróbować ponownie, ponownie udziel upoważnienia." +msgstr "Autoryzacja podpisu wygasła przed jego złożeniem. Spróbuj ponownie." #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "Your signing certificate is invalid, expired, or missing a required key. Contact your administrator or signing provider for assistance." -msgstr "Twój certyfikat podpisu jest nieprawidłowy, wygasł lub brakuje w nim wymaganego klucza. Skontaktuj się z administratorem lub dostawcą usługi podpisu, aby uzyskać pomoc." +msgstr "Certyfikat podpisu jest nieważny, wygasł lub brakuje w nim wymaganego klucza. Skontaktuj się z administratorem lub dostawcą podpisu." #: packages/lib/utils/document-audit-logs.ts msgid "Your signing provider authentication failed" -msgstr "Uwierzytelnianie u Twojego dostawcy usługi podpisu nie powiodło się" +msgstr "Twoje uwierzytelnienie u dostawcy podpisu nie powiodło się" #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "Your signing provider does not advertise a signing algorithm this document accepts. Contact your administrator or signing provider for assistance." -msgstr "Twój dostawca usługi podpisu nie udostępnia algorytmu podpisu akceptowanego przez ten dokument. Skontaktuj się z administratorem lub dostawcą usługi podpisu, aby uzyskać pomoc." +msgstr "Dostawca podpisu nie obsługuje wymaganego algorytmu podpisywania. Skontaktuj się z administratorem lub dostawcą podpisu." #: apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx msgid "Your signing provider returned no usable credentials for this account. Contact your administrator or signing provider for assistance." -msgstr "Twój dostawca usługi podpisu nie zwrócił żadnych użytecznych poświadczeń dla tego konta. Skontaktuj się z administratorem lub dostawcą usługi podpisu, aby uzyskać pomoc." +msgstr "Dostawca podpisu nie zwrócił żadnych użytecznych certyfikatów. Skontaktuj się z administratorem lub dostawcą podpisu." #: apps/remix/app/components/embed/embed-recipient-expired.tsx msgid "Your signing window for this document has expired. Please contact the sender for a new invitation." @@ -14662,7 +14663,7 @@ msgstr "Twój szablon został pomyślnie utworzony" #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx msgid "Your template has been resent successfully." -msgstr "Twój szablon został pomyślnie ponownie wysłany." +msgstr "Szablon został ponownie wysłany." #: apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx msgid "Your template has been successfully duplicated." @@ -14715,5 +14716,5 @@ msgstr "Twój kod weryfikacyjny:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" -msgstr "your-domain.com another-domain.com" +msgstr "twoja-domena.pl inna-domena.pl" From 9c5eb43a26ed8d874b81e72b43c9c0e94a11d22e Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 25 Jun 2026 13:57:17 +1000 Subject: [PATCH 02/41] fix: migrate emails to jobs (#3024) --- .../signing/csc/execute-tsp-sign.ts | 10 +- packages/lib/jobs/client.ts | 6 + .../send-document-deleted-emails.handler.ts | 70 ++++++++++++ .../emails/send-document-deleted-emails.ts | 52 +++++++++ .../send-document-pending-email.handler.ts} | 35 +++--- .../emails/send-document-pending-email.ts | 30 +++++ .../send-recipient-removed-email.handler.ts | 105 ++++++++++++++++++ .../emails/send-recipient-removed-email.ts | 34 ++++++ .../document/complete-document-with-token.ts | 9 +- .../server-only/document/delete-document.ts | 79 ++++++------- .../recipient/delete-envelope-recipient.ts | 85 ++------------ .../recipient/set-document-recipients.ts | 100 ++++------------- 12 files changed, 387 insertions(+), 228 deletions(-) create mode 100644 packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts create mode 100644 packages/lib/jobs/definitions/emails/send-document-deleted-emails.ts rename packages/lib/{server-only/document/send-pending-email.ts => jobs/definitions/emails/send-document-pending-email.handler.ts} (66%) create mode 100644 packages/lib/jobs/definitions/emails/send-document-pending-email.ts create mode 100644 packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts create mode 100644 packages/lib/jobs/definitions/emails/send-recipient-removed-email.ts diff --git a/packages/ee/server-only/signing/csc/execute-tsp-sign.ts b/packages/ee/server-only/signing/csc/execute-tsp-sign.ts index 5045b33d8..3c95720f6 100644 --- a/packages/ee/server-only/signing/csc/execute-tsp-sign.ts +++ b/packages/ee/server-only/signing/csc/execute-tsp-sign.ts @@ -1,6 +1,5 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { jobs } from '@documenso/lib/jobs/client'; -import { sendPendingEmail } from '@documenso/lib/server-only/document/send-pending-email'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { triggerWebhook } from '@documenso/lib/server-only/webhooks/trigger/trigger-webhook'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -474,9 +473,12 @@ export const executeTspSign = async (opts: ExecuteTspSignOptions): Promise 0) { - await sendPendingEmail({ - id: { type: 'envelopeId', id: envelope.id }, - recipientId: recipient.id, + await jobs.triggerJob({ + name: 'send.document.pending.email', + payload: { + envelopeId: envelope.id, + recipientId: recipient.id, + }, }); // TSP envelopes are forced SEQUENTIAL at send-time; this branch always diff --git a/packages/lib/jobs/client.ts b/packages/lib/jobs/client.ts index ae23cb092..397108f61 100644 --- a/packages/lib/jobs/client.ts +++ b/packages/lib/jobs/client.ts @@ -4,11 +4,14 @@ import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/sen import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails'; import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails'; import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email'; +import { SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-deleted-emails'; +import { SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-pending-email'; import { SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-alert-email'; import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email'; import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email'; import { SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-owner-recipient-expired-email'; import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email'; +import { SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-removed-email'; import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email'; import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails'; import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email'; @@ -44,9 +47,12 @@ export const jobsClient = new JobClient([ SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION, SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION, SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION, + SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION, SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION, + SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION, SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION, SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION, + SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION, SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION, BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION, BULK_SEND_TEMPLATE_JOB_DEFINITION, diff --git a/packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts new file mode 100644 index 000000000..4e1aa9fea --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts @@ -0,0 +1,70 @@ +import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; +import { msg } from '@lingui/core/macro'; +import { createElement } from 'react'; + +import { getI18nInstance } from '../../../client-only/providers/i18n-server'; +import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; +import { getEmailContext } from '../../../server-only/email/get-email-context'; +import { isRecipientEmailValidForSending } from '../../../utils/recipients'; +import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; +import type { JobRunIO } from '../../client/_internal/job'; +import type { TSendDocumentDeletedEmailsJobDefinition } from './send-document-deleted-emails'; + +export const run = async ({ payload, io }: { payload: TSendDocumentDeletedEmailsJobDefinition; io: JobRunIO }) => { + const { teamId, documentName, inviterName, inviterEmail, meta, recipients } = payload; + + if (recipients.length === 0) { + return; + } + + const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({ + emailType: 'RECIPIENT', + source: { + type: 'team', + teamId, + }, + meta, + }); + + // Don't send cancellation emails if the organisation has email sending + // disabled. Re-checked here (not just at enqueue time) because the org can be + // disabled between the delete request and this job running. + if (emailsDisabled) { + return; + } + + const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; + const i18n = await getI18nInstance(emailLanguage); + + for (const recipient of recipients) { + await io.runTask(`send-document-deleted-emails-${recipient.email}`, async () => { + if (!isRecipientEmailValidForSending(recipient)) { + return; + } + + const template = createElement(DocumentCancelTemplate, { + documentName, + inviterName: inviterName || undefined, + inviterEmail, + assetBaseUrl, + }); + + const [html, text] = await Promise.all([ + renderEmailWithI18N(template, { lang: emailLanguage, branding }), + renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }), + ]); + + await emailTransport.sendMail({ + to: { + address: recipient.email, + name: recipient.name, + }, + from: senderEmail, + replyTo: replyToEmail, + subject: i18n._(msg`Document Cancelled`), + html, + text, + }); + }); + } +}; diff --git a/packages/lib/jobs/definitions/emails/send-document-deleted-emails.ts b/packages/lib/jobs/definitions/emails/send-document-deleted-emails.ts new file mode 100644 index 000000000..e63de8f6e --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-document-deleted-emails.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; + +import type { JobDefinition } from '../../client/_internal/job'; + +const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID = 'send.document.deleted.emails'; + +const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA = z.object({ + teamId: z.number(), + documentName: z.string(), + inviterName: z.string().optional(), + inviterEmail: z.string(), + /** + * The document's email meta (sender, reply-to, language). Captured before the + * envelope is hard-deleted so `getEmailContext` resolves the exact same + * sender/reply-to/language the inline send used. + */ + meta: z + .object({ + emailId: z.string().nullable().optional(), + emailReplyTo: z.string().nullable().optional(), + language: z.string().optional(), + }) + .nullable(), + recipients: z + .object({ + email: z.string(), + name: z.string(), + }) + .array(), +}); + +export type TSendDocumentDeletedEmailsJobDefinition = z.infer< + typeof SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA +>; + +export const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION = { + id: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID, + name: 'Send Document Deleted Emails', + version: '1.0.0', + trigger: { + name: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID, + schema: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA, + }, + handler: async ({ payload, io }) => { + const handler = await import('./send-document-deleted-emails.handler'); + + await handler.run({ payload, io }); + }, +} as const satisfies JobDefinition< + typeof SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID, + TSendDocumentDeletedEmailsJobDefinition +>; diff --git a/packages/lib/server-only/document/send-pending-email.ts b/packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts similarity index 66% rename from packages/lib/server-only/document/send-pending-email.ts rename to packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts index 4d803f5dd..3a7bf3e96 100644 --- a/packages/lib/server-only/document/send-pending-email.ts +++ b/packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts @@ -1,27 +1,24 @@ import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; +import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { msg } from '@lingui/core/macro'; import { EnvelopeType } from '@prisma/client'; import { createElement } from 'react'; +import { getI18nInstance } from '../../../client-only/providers/i18n-server'; +import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; +import { getEmailContext } from '../../../server-only/email/get-email-context'; +import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; +import { isRecipientEmailValidForSending } from '../../../utils/recipients'; +import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; +import type { JobRunIO } from '../../client/_internal/job'; +import type { TSendDocumentPendingEmailJobDefinition } from './send-document-pending-email'; -import { getI18nInstance } from '../../client-only/providers/i18n-server'; -import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; -import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; -import type { EnvelopeIdOptions } from '../../utils/envelope'; -import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; -import { isRecipientEmailValidForSending } from '../../utils/recipients'; -import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; -import { getEmailContext } from '../email/get-email-context'; +export const run = async ({ payload }: { payload: TSendDocumentPendingEmailJobDefinition; io: JobRunIO }) => { + const { envelopeId, recipientId } = payload; -export interface SendPendingEmailOptions { - id: EnvelopeIdOptions; - recipientId: number; -} - -export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOptions) => { const envelope = await prisma.envelope.findFirst({ where: { - ...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), + ...unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT), recipients: { some: { id: recipientId, @@ -38,12 +35,8 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti }, }); - if (!envelope) { - throw new Error('Document not found'); - } - - if (envelope.recipients.length === 0) { - throw new Error('Document has no recipients'); + if (!envelope || envelope.recipients.length === 0) { + return; } const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({ diff --git a/packages/lib/jobs/definitions/emails/send-document-pending-email.ts b/packages/lib/jobs/definitions/emails/send-document-pending-email.ts new file mode 100644 index 000000000..ff6b885c7 --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-document-pending-email.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; + +import type { JobDefinition } from '../../client/_internal/job'; + +const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID = 'send.document.pending.email'; + +const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({ + envelopeId: z.string(), + recipientId: z.number(), +}); + +export type TSendDocumentPendingEmailJobDefinition = z.infer; + +export const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION = { + id: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID, + name: 'Send Document Pending Email', + version: '1.0.0', + trigger: { + name: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID, + schema: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_SCHEMA, + }, + handler: async ({ payload, io }) => { + const handler = await import('./send-document-pending-email.handler'); + + await handler.run({ payload, io }); + }, +} as const satisfies JobDefinition< + typeof SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID, + TSendDocumentPendingEmailJobDefinition +>; diff --git a/packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts b/packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts new file mode 100644 index 000000000..8f402704e --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts @@ -0,0 +1,105 @@ +import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document'; +import { prisma } from '@documenso/prisma'; +import { msg } from '@lingui/core/macro'; +import { createElement } from 'react'; + +import { getI18nInstance } from '../../../client-only/providers/i18n-server'; +import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; +import { getEmailContext } from '../../../server-only/email/get-email-context'; +import { assertOrganisationRatesAndLimits } from '../../../server-only/rate-limit/assert-organisation-rates-and-limits'; +import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; +import { isRecipientEmailValidForSending } from '../../../utils/recipients'; +import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; +import type { JobRunIO } from '../../client/_internal/job'; +import type { TSendRecipientRemovedEmailJobDefinition } from './send-recipient-removed-email'; + +export const run = async ({ payload, io }: { payload: TSendRecipientRemovedEmailJobDefinition; io: JobRunIO }) => { + const { envelopeId, recipientEmail, recipientName, inviterName } = payload; + + const envelope = await prisma.envelope.findFirst({ + where: { + id: envelopeId, + }, + include: { + documentMeta: true, + }, + }); + + // The envelope may have been deleted between the recipient removal and this + // job running. Treat as a no-op so the job doesn't retry forever. + if (!envelope || !recipientEmail || !isRecipientEmailValidForSending({ email: recipientEmail })) { + return; + } + + // Re-checked at send time (not just at enqueue) so the email honors the + // document's current "recipient removed" setting. + const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientRemoved; + + if (!isRecipientRemovedEmailEnabled) { + return; + } + + const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled, emailTransport } = + await getEmailContext({ + emailType: 'RECIPIENT', + source: { + type: 'team', + teamId: envelope.teamId, + }, + meta: envelope.documentMeta, + }); + + // Don't send the removal email if the organisation has email sending disabled. + if (emailsDisabled) { + return; + } + + // Meter the removal email against the organisation email quota/stats. + // Add/remove churn can be used to blast unsolicited removal emails outside + // the email limits. + try { + await assertOrganisationRatesAndLimits({ + organisationId, + organisationClaim: claims, + type: 'email', + count: 1, + }); + } catch (_err) { + io.logger.warn({ + msg: 'Recipient removed email dropped: org email limit exceeded', + organisationId, + envelopeId: envelope.id, + }); + + return; + } + + const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; + + const template = createElement(RecipientRemovedFromDocumentTemplate, { + documentName: envelope.title, + inviterName: inviterName || undefined, + assetBaseUrl, + }); + + const i18n = await getI18nInstance(emailLanguage); + + await io.runTask('send-recipient-removed-email', async () => { + const [html, text] = await Promise.all([ + renderEmailWithI18N(template, { lang: emailLanguage, branding }), + renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }), + ]); + + await emailTransport.sendMail({ + to: { + address: recipientEmail, + name: recipientName, + }, + from: senderEmail, + replyTo: replyToEmail, + subject: i18n._(msg`You have been removed from a document`), + html, + text, + }); + }); +}; diff --git a/packages/lib/jobs/definitions/emails/send-recipient-removed-email.ts b/packages/lib/jobs/definitions/emails/send-recipient-removed-email.ts new file mode 100644 index 000000000..484c70ac0 --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-recipient-removed-email.ts @@ -0,0 +1,34 @@ +import { z } from 'zod'; + +import type { JobDefinition } from '../../client/_internal/job'; + +const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID = 'send.recipient.removed.email'; + +const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({ + envelopeId: z.string(), + recipientEmail: z.string(), + recipientName: z.string(), + inviterName: z.string().optional(), +}); + +export type TSendRecipientRemovedEmailJobDefinition = z.infer< + typeof SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA +>; + +export const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION = { + id: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID, + name: 'Send Recipient Removed Email', + version: '1.0.0', + trigger: { + name: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID, + schema: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA, + }, + handler: async ({ payload, io }) => { + const handler = await import('./send-recipient-removed-email.handler'); + + await handler.run({ payload, io }); + }, +} as const satisfies JobDefinition< + typeof SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID, + TSendRecipientRemovedEmailJobDefinition +>; diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index 531cc59f4..10aefd189 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -29,7 +29,6 @@ import { assertRecipientNotExpired } from '../../utils/recipients'; import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { isRecipientAuthorized } from './is-recipient-authorized'; -import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; @@ -389,7 +388,13 @@ export const completeDocumentWithToken = async ({ }); if (pendingRecipients.length > 0) { - await sendPendingEmail({ id, recipientId: recipient.id }); + await jobs.triggerJob({ + name: 'send.document.pending.email', + payload: { + envelopeId: envelope.id, + recipientId: recipient.id, + }, + }); if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { const [nextRecipient] = pendingRecipients; diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index 3711921c6..8eed2702f 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -1,13 +1,9 @@ -import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import { prisma } from '@documenso/prisma'; -import { msg } from '@lingui/core/macro'; import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client'; import { DocumentStatus, EnvelopeType, RecipientRole, SendStatus, WebhookTriggerEvents } from '@prisma/client'; -import { createElement } from 'react'; -import { getI18nInstance } from '../../client-only/providers/i18n-server'; -import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { jobs } from '../../jobs/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload'; @@ -16,7 +12,6 @@ import { isDocumentCompleted } from '../../utils/document'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { isRecipientEmailValidForSending } from '../../utils/recipients'; -import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; import { getMemberRoles } from '../team/get-member-roles'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -125,7 +120,7 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha return; } - const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({ + const { emailLanguage, emailsDisabled } = await getEmailContext({ emailType: 'RECIPIENT', source: { type: 'team', @@ -192,50 +187,40 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha return deletedEnvelope; } - // Send cancellation emails to recipients. - await Promise.all( - envelope.recipients.map(async (recipient) => { - if ( - recipient.sendStatus !== SendStatus.SENT || - !isRecipientEmailValidForSending(recipient) || - recipient.role === RecipientRole.CC - ) { - return; - } + // Enqueue cancellation emails as a background job. The envelope (and its + // documentMeta) is hard-deleted above, so the job can't look it up later — + // pass a self-contained payload with the recipients to notify. + const recipientsToNotify = envelope.recipients + .filter( + (recipient) => + recipient.sendStatus === SendStatus.SENT && + recipient.role !== RecipientRole.CC && + isRecipientEmailValidForSending(recipient), + ) + .map((recipient) => ({ + email: recipient.email, + name: recipient.name, + })); - const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; - - const template = createElement(DocumentCancelTemplate, { + if (recipientsToNotify.length > 0) { + await jobs.triggerJob({ + name: 'send.document.deleted.emails', + payload: { + teamId: envelope.teamId, documentName: envelope.title, inviterName: user.name || undefined, inviterEmail: user.email, - assetBaseUrl, - }); - - const [html, text] = await Promise.all([ - renderEmailWithI18N(template, { lang: emailLanguage, branding }), - renderEmailWithI18N(template, { - lang: emailLanguage, - branding, - plainText: true, - }), - ]); - - const i18n = await getI18nInstance(emailLanguage); - - await emailTransport.sendMail({ - to: { - address: recipient.email, - name: recipient.name, - }, - from: senderEmail, - replyTo: replyToEmail, - subject: i18n._(msg`Document Cancelled`), - html, - text, - }); - }), - ); + meta: envelope.documentMeta + ? { + emailId: envelope.documentMeta.emailId, + emailReplyTo: envelope.documentMeta.emailReplyTo, + language: emailLanguage, + } + : null, + recipients: recipientsToNotify, + }, + }); + } return deletedEnvelope; }; diff --git a/packages/lib/server-only/recipient/delete-envelope-recipient.ts b/packages/lib/server-only/recipient/delete-envelope-recipient.ts index 06d90ca46..e2e99feed 100644 --- a/packages/lib/server-only/recipient/delete-envelope-recipient.ts +++ b/packages/lib/server-only/recipient/delete-envelope-recipient.ts @@ -1,24 +1,16 @@ -import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { prisma } from '@documenso/prisma'; -import { msg } from '@lingui/core/macro'; import { EnvelopeType, RecipientRole, SendStatus } from '@prisma/client'; -import { createElement } from 'react'; -import { getI18nInstance } from '../../client-only/providers/i18n-server'; -import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { jobs } from '../../jobs/client'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; -import { logger } from '../../utils/logger'; import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients'; -import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { buildTeamWhereQuery } from '../../utils/teams'; -import { getEmailContext } from '../email/get-email-context'; import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; -import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; export interface DeleteEnvelopeRecipientOptions { userId: number; @@ -148,75 +140,16 @@ export const deleteEnvelopeRecipient = async ({ envelope.type === EnvelopeType.DOCUMENT && isRecipientEmailValidForSending(recipientToDelete) ) { - const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; - - const template = createElement(RecipientRemovedFromDocumentTemplate, { - documentName: envelope.title, - inviterName: envelope.team?.name || user.name || undefined, - assetBaseUrl, - }); - - const { - branding, - emailLanguage, - senderEmail, - replyToEmail, - organisationId, - claims, - emailsDisabled, - emailTransport, - } = await getEmailContext({ - emailType: 'RECIPIENT', - source: { - type: 'team', - teamId: envelope.teamId, - }, - meta: envelope.documentMeta, - }); - - // Don't send the removal email if the organisation has email sending disabled. - if (emailsDisabled) { - return deletedRecipient; - } - - // Meter the removal email against the organisation email quota/stats. - // Add/remove churn can be used to blast unsolicited removal emails - // outside the email limits. - try { - await assertOrganisationRatesAndLimits({ - organisationId, - organisationClaim: claims, - type: 'email', - count: 1, - }); - } catch (_err) { - logger.warn({ - msg: 'Recipient removed email dropped: org email limit exceeded', - organisationId, - recipientId: recipientToDelete.id, + // Enqueue the "removed from document" email as a background job so a + // transient mail outage doesn't fail the request and the send is retried. + await jobs.triggerJob({ + name: 'send.recipient.removed.email', + payload: { envelopeId: envelope.id, - }); - - return deletedRecipient; - } - - const [html, text] = await Promise.all([ - renderEmailWithI18N(template, { lang: emailLanguage, branding }), - renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }), - ]); - - const i18n = await getI18nInstance(emailLanguage); - - await emailTransport.sendMail({ - to: { - address: recipientToDelete.email, - name: recipientToDelete.name, + recipientEmail: recipientToDelete.email, + recipientName: recipientToDelete.name, + inviterName: envelope.team?.name || user.name || undefined, }, - from: senderEmail, - replyTo: replyToEmail, - subject: i18n._(msg`You have been removed from a document`), - html, - text, }); } diff --git a/packages/lib/server-only/recipient/set-document-recipients.ts b/packages/lib/server-only/recipient/set-document-recipients.ts index 2e58936ab..ca2ca666d 100644 --- a/packages/lib/server-only/recipient/set-document-recipients.ts +++ b/packages/lib/server-only/recipient/set-document-recipients.ts @@ -1,4 +1,3 @@ -import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth'; import { type TRecipientActionAuthTypes, ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth'; @@ -7,25 +6,18 @@ import { nanoid } from '@documenso/lib/universal/id'; import { createDocumentAuditLogData, diffRecipientChanges } from '@documenso/lib/utils/document-audit-logs'; import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth'; import { prisma } from '@documenso/prisma'; -import { msg } from '@lingui/core/macro'; import type { Recipient } from '@prisma/client'; import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client'; -import { createElement } from 'react'; import { isDeepEqual } from 'remeda'; -import { getI18nInstance } from '../../client-only/providers/i18n-server'; -import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { jobs } from '../../jobs/client'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope'; -import { logger } from '../../utils/logger'; import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients'; -import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; -import { getEmailContext } from '../email/get-email-context'; import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role'; -import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; export interface SetDocumentRecipientsOptions { userId: number; @@ -88,16 +80,6 @@ export const setDocumentRecipients = async ({ throw new Error('Document already complete'); } - const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled, emailTransport } = - await getEmailContext({ - emailType: 'RECIPIENT', - source: { - type: 'team', - teamId, - }, - meta: envelope.documentMeta, - }); - const recipientsHaveActionAuth = recipients.some( (recipient) => recipient.actionAuth && recipient.actionAuth.length > 0, ); @@ -290,67 +272,29 @@ export const setDocumentRecipients = async ({ const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientRemoved; - // Send emails to deleted recipients who have emails. - await Promise.all( - removedRecipients.map(async (recipient) => { - if ( - emailsDisabled || - recipient.sendStatus !== SendStatus.SENT || - recipient.role === RecipientRole.CC || - !isRecipientRemovedEmailEnabled || - !isRecipientEmailValidForSending(recipient) - ) { - return; - } + if (isRecipientRemovedEmailEnabled) { + await Promise.all( + removedRecipients.map(async (recipient) => { + if ( + recipient.sendStatus !== SendStatus.SENT || + recipient.role === RecipientRole.CC || + !isRecipientEmailValidForSending(recipient) + ) { + return; + } - // Meter against the organisation email quota/stats so add/remove churn - // can't be used to send unsolicited "removed" emails outside the limits. - try { - await assertOrganisationRatesAndLimits({ - organisationId, - organisationClaim: claims, - type: 'email', - count: 1, + await jobs.triggerJob({ + name: 'send.recipient.removed.email', + payload: { + envelopeId: envelope.id, + recipientEmail: recipient.email, + recipientName: recipient.name, + inviterName: user.name || undefined, + }, }); - } catch (_err) { - logger.warn({ - msg: 'Recipient removed email dropped: org email limit exceeded', - organisationId, - recipientId: recipient.id, - envelopeId: envelope.id, - }); - - return; - } - - const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; - - const template = createElement(RecipientRemovedFromDocumentTemplate, { - documentName: envelope.title, - inviterName: user.name || undefined, - assetBaseUrl, - }); - - const [html, text] = await Promise.all([ - renderEmailWithI18N(template, { lang: emailLanguage, branding }), - renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }), - ]); - - const i18n = await getI18nInstance(emailLanguage); - - await emailTransport.sendMail({ - to: { - address: recipient.email, - name: recipient.name, - }, - from: senderEmail, - replyTo: replyToEmail, - subject: i18n._(msg`You have been removed from a document`), - html, - text, - }); - }), - ); + }), + ); + } } // Filter out recipients that have been removed or have been updated. From 94adea149de72c2abc9bdfe039b844d25b821117 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 25 Jun 2026 23:59:25 +1000 Subject: [PATCH 03/41] chore: general repo maintenance and docs cleanup (#3030) Refresh README/docs for the current stack, add a security policy, note the external-PR pause, and remove dead config and workflows. --- .env.example | 2 +- .github/ISSUE_TEMPLATE/bug-report.yml | 3 +- .github/ISSUE_TEMPLATE/config.yml | 11 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 1 - .github/ISSUE_TEMPLATE/improvement.yml | 11 -- .github/PULL_REQUEST_TEMPLATE.md | 11 ++ .../PULL_REQUEST_TEMPLATE/test-addition.md | 40 ------- .github/workflows/first-interaction.yml | 8 +- .github/workflows/issue-assignee-check.yml | 62 ----------- .github/workflows/pr-review-reminder.yml | 63 ----------- .github/workflows/semantic-pull-requests.yml | 27 ----- .github/workflows/stale.yml | 2 +- .gitpod.yml | 3 +- .well-known/security.txt | 15 ++- CONTRIBUTING.md | 26 +++-- README.md | 34 +++--- SECURITY.md | 38 +++++++ SIGNING.md | 70 ++---------- apps/docs/README.md | 47 ++------ .../developers/local-development/index.mdx | 15 +-- .../docs/self-hosting/configuration/email.mdx | 4 +- .../configuration/environment.mdx | 15 +++ .../self-hosting/deployment/kubernetes.mdx | 2 +- .../docs/self-hosting/deployment/manual.mdx | 4 +- .../getting-started/quick-start.mdx | 16 +-- .../getting-started/requirements.mdx | 6 +- .../self-hosting/getting-started/tips.mdx | 2 +- apps/docs/content/docs/self-hosting/index.mdx | 8 ++ .../self-hosting/maintenance/upgrades.mdx | 20 ++-- apps/remix/Dockerfile | 22 ---- apps/remix/README.md | 100 ++---------------- apps/remix/public/.well-known/security.txt | 15 ++- package.json | 1 - .../webhooks/assert-webhook-url.ts | 13 ++- .../server-only/webhooks/is-private-url.ts | 7 +- render.yaml | 2 +- tsconfig.eslint.json | 4 - 37 files changed, 227 insertions(+), 503 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/PULL_REQUEST_TEMPLATE/test-addition.md delete mode 100644 .github/workflows/issue-assignee-check.yml delete mode 100644 .github/workflows/pr-review-reminder.yml create mode 100644 SECURITY.md delete mode 100644 apps/remix/Dockerfile delete mode 100644 tsconfig.eslint.json diff --git a/.env.example b/.env.example index aa6565f8c..be3ecaab3 100644 --- a/.env.example +++ b/.env.example @@ -103,7 +103,7 @@ NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso" NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password" # [[SMTP]] -# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels +# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | resend | mailchannels NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" # OPTIONAL: Defines the host to use for sending emails. NEXT_PRIVATE_SMTP_HOST="127.0.0.1" diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 4fcde0ea3..ceee6933e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -34,7 +34,7 @@ body: label: Browser [e.g., Chrome, Firefox] - type: input attributes: - label: Version [e.g., 2.0.1] + label: Version [e.g., 2.13.0] - type: checkboxes attributes: label: Please check the boxes that apply to this issue report. @@ -44,4 +44,3 @@ body: - label: I have included relevant environment information. - label: I have included any relevant screenshots. - label: I understand that this is a voluntary contribution and that there is no guarantee of resolution. - - label: I want to work on creating a PR for this issue if approved diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..4be27fb49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/documenso/documenso/security/advisories/new + about: Please report security vulnerabilities privately via GitHub Security Advisories. Do not open a public issue. + - name: Questions & Discussions + url: https://github.com/documenso/documenso/discussions + about: Ask questions, share ideas, and discuss Documenso with the community. + - name: Discord + url: https://documen.so/discord + about: Chat with the community and the team. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index ffb788c23..ab21e8828 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -33,4 +33,3 @@ body: - label: I have explained the use case or scenario for this feature. - label: I have included any relevant technical details or design suggestions. - label: I understand that this is a suggestion and that there is no guarantee of implementation. - - label: I want to work on creating a PR for this issue if approved diff --git a/.github/ISSUE_TEMPLATE/improvement.yml b/.github/ISSUE_TEMPLATE/improvement.yml index de2983b67..424d54a53 100644 --- a/.github/ISSUE_TEMPLATE/improvement.yml +++ b/.github/ISSUE_TEMPLATE/improvement.yml @@ -15,17 +15,6 @@ body: description: 'Are there any additional context or information that might be relevant to the improvement suggestion.' validations: required: false - - type: dropdown - id: assignee - attributes: - label: 'Do you want to work on this improvement?' - multiple: false - options: - - 'No' - - 'Yes' - default: 0 - validations: - required: true - type: checkboxes attributes: label: 'Please check the boxes that apply to this improvement suggestion.' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 66602d12b..4e6151b34 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,14 @@ + + ## Description diff --git a/.github/PULL_REQUEST_TEMPLATE/test-addition.md b/.github/PULL_REQUEST_TEMPLATE/test-addition.md deleted file mode 100644 index f93c81493..000000000 --- a/.github/PULL_REQUEST_TEMPLATE/test-addition.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: Test Addition -about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion ---- - -## Description - - - - -## Related Issue - - - - -## Test Details - - - - -- Test Name: Name of the test -- Type: [Unit / E2E] -- Description: Brief description of what the test checks -- Inputs: What inputs the test uses (if applicable) -- Expected Output: What output or behavior the test expects - -## Checklist - - - - -- [ ] I have written the new test and ensured it works as intended. -- [ ] I have added necessary documentation to explain the purpose of the test. -- [ ] I have followed the project's testing guidelines and coding style. -- [ ] I have addressed any review feedback from previous submissions, if applicable. - -## Additional Notes - - - diff --git a/.github/workflows/first-interaction.yml b/.github/workflows/first-interaction.yml index 5f53eb280..a4d7c8a54 100644 --- a/.github/workflows/first-interaction.yml +++ b/.github/workflows/first-interaction.yml @@ -1,13 +1,10 @@ name: 'Welcome New Contributors' on: - pull_request: - types: ['opened'] issues: types: ['opened'] permissions: - pull-requests: write issues: write jobs: @@ -20,10 +17,7 @@ jobs: - uses: actions/first-interaction@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: | - Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀 -
Feel free to hop into our community in [Discord](https://documen.so/discord) issue-message: | Thank you for opening your first issue and for being a part of the open signing revolution! -
One of our team members will review it and get back to you as soon as it possible 💚 +
One of our team members will review it and get back to you as soon as possible 💚
Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord) diff --git a/.github/workflows/issue-assignee-check.yml b/.github/workflows/issue-assignee-check.yml deleted file mode 100644 index de53564ec..000000000 --- a/.github/workflows/issue-assignee-check.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: 'Issue Assignee Check' - -on: - issues: - types: ['assigned'] - -permissions: - issues: write - -jobs: - countIssues: - if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Install Octokit - run: npm install @octokit/rest@18 - - - name: Check Assigned User's Issue Count - id: parse-comment - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { Octokit } = require("@octokit/rest"); - const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); - - const username = context.payload.issue.assignee.login; - console.log(`Username Extracted: ${username}`); - - const { data: issues } = await octokit.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - assignee: username, - state: 'open' - }); - - const issueCount = issues.length; - console.log(`Issue Count For ${username}: ${issueCount}`); - - if (issueCount > 3) { - let issueCountMessage = `### 🚨 Documenso Police 🚨`; - issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`; - - await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: issueCountMessage, - headers: { - 'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`, - } - }); - } diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml deleted file mode 100644 index 67dc32f34..000000000 --- a/.github/workflows/pr-review-reminder.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: 'PR Review Reminder' - -on: - pull_request: - types: ['opened', 'ready_for_review'] - -permissions: - pull-requests: write - -jobs: - checkPRs: - if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review') - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Install Octokit - run: npm install @octokit/rest@18 - - - name: Check user's PRs awaiting review - id: parse-prs - uses: actions/github-script@v5 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { Octokit } = require("@octokit/rest"); - const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); - - const username = context.payload.pull_request.user.login; - console.log(`Username Extracted: ${username}`); - - const { data: pullRequests } = await octokit.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - sort: 'created', - direction: 'asc', - }); - - const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review')); - const prCount = userPullRequests.length; - console.log(`PR Count for ${username}: ${prCount}`); - - if (prCount > 3) { - let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`; - - await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - body: prReminderMessage, - headers: { - 'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`, - } - }); - } diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index 76a1b2f42..0dab3392d 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -16,24 +16,6 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - name: Check PR creator's previous activity - id: check_activity - run: | - CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login') - ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count') - if [ "$ACTIVITY" -eq 0 ]; then - echo "::set-output name=is_new::true" - else - echo "::set-output name=is_new::false" - fi - - - name: Count PRs created by user - id: count_prs - run: | - CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login') - PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count') - echo "::set-output name=pr_count::$PR_COUNT" - - uses: amannn/action-semantic-pull-request@v5 id: lint_pr_title env: @@ -44,8 +26,6 @@ jobs: with: header: pr-title-lint-error message: | - Hey There! and thank you for opening this pull request! 📝👋🏼 - We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. Details: @@ -53,10 +33,3 @@ jobs: ``` ${{ steps.lint_pr_title.outputs.error_message }} ``` - - - if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}} - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pr-title-lint-error - message: | - Thank you for following the naming conventions for pull request titles! 💚🚀 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a18e33f87..c9c12ce59 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -21,4 +21,4 @@ jobs: stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.' close-pr-message: 'This PR has been closed because of inactivity.' exempt-pr-labels: 'WIP,on-hold,needs review' - exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage' + exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,status: assigned,status: triage' diff --git a/.gitpod.yml b/.gitpod.yml index 261f8c96b..883ac5bb3 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -31,8 +31,7 @@ vscode: extensions: - aaron-bond.better-comments - bradlc.vscode-tailwindcss - - dbaeumer.vscode-eslint - - esbenp.prettier-vscode + - biomejs.biome - mikestead.dotenv - unifiedjs.vscode-mdx - GitHub.vscode-pull-request-github diff --git a/.well-known/security.txt b/.well-known/security.txt index 1a3f685e5..f96fce0f0 100644 --- a/.well-known/security.txt +++ b/.well-known/security.txt @@ -1,7 +1,14 @@ -# General Issues +# Report security vulnerabilities privately via GitHub Security Advisories (preferred). +Contact: https://github.com/documenso/documenso/security/advisories/new + +# Alternatively, report critical issues privately by email. +Contact: mailto:security@documenso.com + +# Security policy +Policy: https://github.com/documenso/documenso/security/policy + +# General (non-security) issues Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml -# Report critical issues privately to let us take appropriate action before publishing. -Contact: mailto:security@documenso.com Preferred-Languages: en -Canonical: https://documenso.com/.well-known/security.txt \ No newline at end of file +Canonical: https://documenso.com/.well-known/security.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cd7a6887..7260cdfe2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,13 +1,27 @@ # Contributing to Documenso -If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated. +> **We are no longer accepting external pull requests.** +> +> Aside from a small group of trusted contributors we reach out to directly, we no longer merge external PRs. New pull requests will usually be closed with a request to open an issue instead. This is a security decision, not a judgement on your work. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the full reasoning. +> +> Documenso stays open source. You can still read, audit, run, and fork the code. The best way to contribute is through detailed issues. -## Before getting started +## How to contribute now -- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission. -- Select an issue from [here](https://github.com/documenso/documenso/issues) or create a new one -- Consider the results from the discussion on the issue -- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions. +The most useful contribution is a detailed issue. Treat it like a spec. The more detail, the better: + +- The problem you're trying to solve, and who it affects +- How you expect the feature or change to behave +- Edge cases, constraints, and anything you've already considered +- Examples, mockups, or references where they help + +Before opening an issue, search [existing issues](https://github.com/documenso/documenso/issues) and [discussions](https://github.com/documenso/documenso/discussions) for related items. If a proposal is detailed and fits where Documenso is heading, we'll pick it up and build against it. + +For security vulnerabilities, do not open a public issue. Follow our [Security Policy](./SECURITY.md) instead. + +--- + +The sections below are for trusted contributors working with us directly, and for anyone running Documenso locally or maintaining a fork. ## English only PRs and Issues diff --git a/README.md b/README.md index b52220554..642a285eb 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,18 @@ Join us in creating the next generation of open trust infrastructure. ## Community and Next Steps 🎯 -- Check out the first source code release in this repository and test it. +- Try Documenso by self-hosting it or signing up at [documenso.com](https://documenso.com). - Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions). -- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members. +- Join the [Discord server](https://documen.so/discord) for any questions and getting to know other community members. - ⭐ the repository to help us raise awareness. -- Spread the word on Twitter that Documenso is working towards a more open signing tool. -- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release. +- Open detailed [issues](https://github.com/documenso/documenso/issues) to report bugs or propose features. ## Contributing -- To contribute, please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md). +> **Note**: We no longer accept external pull requests, aside from a small group of trusted contributors we reach out to directly. The best way to contribute is through detailed issues. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the reasoning. + +- Documenso stays open source. You can read, audit, run, and fork the code. +- To report issues or propose changes, see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md). ## Contact us @@ -81,17 +83,21 @@ Contact us if you are interested in our Enterprise plan for large organizations

-- [Typescript](https://www.typescriptlang.org/) - Language -- [ReactRouter](https://reactrouter.com/) - Framework +- [TypeScript](https://www.typescriptlang.org/) - Language +- [React Router v7](https://reactrouter.com/) - Framework +- [Hono](https://hono.dev/) - Server - [Prisma](https://www.prisma.io/) - ORM -- [Tailwind](https://tailwindcss.com/) - CSS -- [shadcn/ui](https://ui.shadcn.com/) - Component Library +- [Tailwind CSS](https://tailwindcss.com/) - CSS +- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library - [react-email](https://react.email/) - Email Templates +- [Lingui](https://lingui.dev/) - Internationalization - [tRPC](https://trpc.io/) - API -- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures (launching soon) -- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs -- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation +- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signatures +- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs +- [@cantoo/pdf-lib](https://github.com/cantoo-scribe/pdf-lib) - PDF manipulation - [Stripe](https://stripe.com/) - Payments +- [Biome](https://biomejs.dev/) - Linting & Formatting +- [Playwright](https://playwright.dev/) - E2E Testing @@ -196,6 +202,10 @@ For full instructions, requirements, and configuration details, see the [Self Ho [![Deploy on Elestio](https://elest.io/images/logos/deploy-to-elestio-btn.png)](https://elest.io/open-source/documenso) +## Security + +If you believe you have found a security vulnerability in Documenso, please report it through our [Security Policy](https://github.com/documenso/documenso/security/policy). We prioritize private reports via [GitHub Security Advisories](https://github.com/documenso/documenso/security/advisories/new). See [SECURITY.md](./SECURITY.md) for scope and details. + ## Troubleshooting For troubleshooting self-hosted deployments, see the [Troubleshooting guide](https://docs.documenso.com/docs/self-hosting/maintenance/troubleshooting) and [Tips & Common Pitfalls](https://docs.documenso.com/docs/self-hosting/getting-started/tips). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..7c672b928 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,38 @@ +# Security Policy + +We take the security of Documenso seriously. As a platform trusted with legally binding documents, the safety of the project and the people who rely on it is a priority for us. We're grateful to the security researchers who help keep it that way. If you've found an issue, we'd genuinely like to hear about it. + +## Reporting a Vulnerability + +Report security vulnerabilities privately. Do not open a public issue, discussion, or pull request for security reports. + +We accept reports through two channels, in order of preference: + +1. **GitHub Security Advisories (preferred)**. Use the [private vulnerability reporting form](https://github.com/documenso/documenso/security/advisories/new). This is our primary channel and lets us triage and work with you on a fix. +2. **Email**. If you cannot use GitHub Security Advisories, email [security@documenso.com](mailto:security@documenso.com). + +Include the affected version, a clear description, steps to reproduce, and the potential impact. + +## Triage and Response + +We triage reports as we have availability. We read every report we receive, and we appreciate the time and effort it takes to put one together. + +We also run [Codex](https://openai.com/codex/) security analysis across the codebase. If Codex has already reported the issue you're sending us, we may close your report as a duplicate. Please don't take this as a reflection on your work; it just means our automated tooling happened to surface the same thing first. + +## Scope + +This policy covers vulnerabilities in the Documenso application code in this repository. + +The items below are out of scope and will not be accepted. They are deployment, infrastructure, and configuration concerns that belong with the operator's firewall, network, and environment setup, not the application: + +- Server-Side Request Forgery (SSRF) and related network-egress concerns +- DNS rebinding and other DNS-level issues +- Rate limiting, denial of service, and volumetric attacks +- TLS and certificate configuration, HTTP security headers, and other reverse-proxy or web-server configuration +- Findings that depend on insecure self-hosted infrastructure or misconfiguration + +If you're unsure whether something is in scope, report it privately anyway and we'll happily take a look. + +## Supported Versions + +Security fixes are applied to the latest release. Run the most recent version of Documenso. diff --git a/SIGNING.md b/SIGNING.md index f794bc9ba..9aed0759a 100644 --- a/SIGNING.md +++ b/SIGNING.md @@ -1,67 +1,9 @@ -# Creating your own signing certificate +# Signing Certificate -For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one: +Documenso needs a signing certificate to digitally sign documents. For full, up-to-date instructions on generating, converting, and configuring a certificate, see the official documentation: -1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key: +- [Signing Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate): Overview and all certificate options +- [Local Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/local): Generate a self-signed `.p12` certificate with OpenSSL +- [Google Cloud HSM](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm): Sign using Google Cloud KMS - `openssl genrsa -out private.key 2048` - -2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate: - - `openssl req -new -x509 -key private.key -out certificate.crt -days 365` - - This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The `-days` parameter sets the number of days for which the certificate is valid. - -3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following commands to do this: - - ```bash - # Set certificate password securely (won't appear in command history) - read -s -p "Enter certificate password: " CERT_PASS - echo - - # Create the p12 certificate using the environment variable - openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \ - -password env:CERT_PASS \ - -keypbe PBE-SHA1-3DES \ - -certpbe PBE-SHA1-3DES \ - -macalg sha1 - ``` - -4. **IMPORTANT**: A certificate password is required to prevent signing failures. Make sure to use a strong password (minimum 4 characters) when prompted. Certificates without passwords will cause "Failed to get private key bags" errors during document signing. - -5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created) - -## Docker - -> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image. - -Want to create a production ready docker image? Follow these steps: - -- cd into `docker` directory -- Make `build.sh` executable by running `chmod +x build.sh` -- Run `./build.sh` to start building the docker image. -- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command - -``` -docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest -``` - -Command Breakdown: - -- `-d` - Let's you run the container in background -- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port. -- `-v` - Volume let's you persist the data -- `--name` - Name of the container -- `documenso:latest` - Image you have built - -## Deployment - -We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates! - -## Railway - -[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic) - -## Render - -[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) +For deploying Documenso with Docker, see the [Docker Deployment](https://docs.documenso.com/docs/self-hosting/deployment/docker) and [Docker Compose](https://docs.documenso.com/docs/self-hosting/deployment/docker-compose) guides. diff --git a/apps/docs/README.md b/apps/docs/README.md index 9b7bba9e0..770d32417 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -1,45 +1,16 @@ -# docs +# @documenso/docs -This is a Next.js application generated with -[Create Fumadocs](https://github.com/fuma-nama/fumadocs). +The Documenso documentation site, built with [Next.js](https://nextjs.org/) and [Fumadocs](https://fumadocs.dev/). Published at [docs.documenso.com](https://docs.documenso.com). -Run development server: +Content lives under `content/docs/` as MDX. See [WRITING_STYLE.md](../../WRITING_STYLE.md) for the documentation writing conventions. ```bash -npm run dev -# or -pnpm dev -# or -yarn dev +# From the monorepo root +npm run dev --filter=@documenso/docs ``` -Open http://localhost:3000 with your browser to see the result. +## Structure -## Explore - -In the project, you can see: - -- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content. -- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep. - -| Route | Description | -| ------------------------- | ------------------------------------------------------ | -| `app/(home)` | The route group for your landing page and other pages. | -| `app/docs` | The documentation layout and pages. | -| `app/api/search/route.ts` | The Route Handler for search. | - -### Fumadocs MDX - -A `source.config.ts` config file has been included, you can customise different options like frontmatter schema. - -Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details. - -## Learn More - -To learn more about Next.js and Fumadocs, take a look at the following -resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js - features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs +- `content/docs/`: Documentation pages (MDX). +- `lib/source.ts`: Content source adapter. +- `lib/layout.shared.tsx`: Shared layout options. diff --git a/apps/docs/content/docs/developers/local-development/index.mdx b/apps/docs/content/docs/developers/local-development/index.mdx index 5714a018b..3092da415 100644 --- a/apps/docs/content/docs/developers/local-development/index.mdx +++ b/apps/docs/content/docs/developers/local-development/index.mdx @@ -15,16 +15,17 @@ Pick the one that fits your needs the best. ## Tech Stack -- [Typescript](https://www.typescriptlang.org/) - Language -- [React Router](https://reactrouter.com/) - Framework +- [TypeScript](https://www.typescriptlang.org/) - Language +- [React Router v7](https://reactrouter.com/) - Framework +- [Hono](https://hono.dev/) - Server - [Prisma](https://www.prisma.io/) - ORM -- [Tailwind](https://tailwindcss.com/) - CSS -- [shadcn/ui](https://ui.shadcn.com/) - Component Library +- [Tailwind CSS](https://tailwindcss.com/) - CSS +- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library - [react-email](https://react.email/) - Email Templates +- [Lingui](https://lingui.dev/) - Internationalization - [tRPC](https://trpc.io/) - API -- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures -- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs -- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation +- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signing and Manipulation +- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs - [Stripe](https://stripe.com/) - Payments
diff --git a/apps/docs/content/docs/self-hosting/configuration/email.mdx b/apps/docs/content/docs/self-hosting/configuration/email.mdx index bc7c729fa..d1c03e6cd 100644 --- a/apps/docs/content/docs/self-hosting/configuration/email.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/email.mdx @@ -278,7 +278,9 @@ Test your email configuration by creating an account or resetting a password. Th ### Using a Test SMTP Server -For development or testing, use a local SMTP server like [Mailhog](https://github.com/mailhog/MailHog) or [Mailpit](https://github.com/axllent/mailpit): +For development or testing, use a local SMTP server like [Inbucket](https://www.inbucket.org/), [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). The default development setup (`docker/development/compose.yml`) already runs Inbucket, with its web UI on port 9000 and SMTP on port 2500. + +To run one standalone instead: ```bash # Using Docker diff --git a/apps/docs/content/docs/self-hosting/configuration/environment.mdx b/apps/docs/content/docs/self-hosting/configuration/environment.mdx index 33f723c31..4ec25e213 100644 --- a/apps/docs/content/docs/self-hosting/configuration/environment.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/environment.mdx @@ -86,6 +86,21 @@ Callback URL: `https:///api/auth/callback/microsoft` | `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts | | `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit | +### Webhooks + +| Variable | Default | Description | +| --------------------------------------- | ------- | ------------------------------------------------------------------------ | +| `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` | - | Comma-separated hostnames or IPs allowed to resolve to private addresses | + +Before delivering a webhook, Documenso checks whether the target resolves to a +private or loopback address and blocks it if so. This check is best-effort and +fails open. Use `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` to allow specific +internal hosts, for example when delivering to a service on your own network: + +```bash +NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS="hooks.internal.example,10.0.0.5" +``` + --- ## Email Configuration diff --git a/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx b/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx index d8adb8ac9..e7a18490f 100644 --- a/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx @@ -235,7 +235,7 @@ spec: ``` - Pin to a specific image tag (e.g., `documenso/documenso:1.5.0`) in production instead of `latest` + Pin to a specific image tag (e.g., `documenso/documenso:`) in production instead of `latest` to ensure predictable deployments. diff --git a/apps/docs/content/docs/self-hosting/deployment/manual.mdx b/apps/docs/content/docs/self-hosting/deployment/manual.mdx index bdd51fd68..d6dc4fda5 100644 --- a/apps/docs/content/docs/self-hosting/deployment/manual.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/manual.mdx @@ -14,8 +14,8 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'; ## Prerequisites -- Node.js 20 or later -- npm +- Node.js 22 or later +- npm 11 or later - PostgreSQL 14 or later - A Linux server (for systemd service setup) diff --git a/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx b/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx index b0cc80ff8..9f913c766 100644 --- a/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx +++ b/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx @@ -124,12 +124,16 @@ docker compose -f docker/development/compose.yml exec database \ The quick start setup runs the following containers: -| Container | Purpose | Port | -| ----------- | -------------------------------- | ----- | -| `documenso` | Main application | 3000 | -| `database` | PostgreSQL database | 54320 | -| `maildev` | Local email testing server | 2500 | -| `minio` | S3-compatible storage (optional) | 9000 | +| Container | Purpose | Port | +| ----------- | ------------------------------------ | ----------------------------- | +| `documenso` | Main application | 3000 | +| `database` | PostgreSQL database | 54320 | +| `inbucket` | Local email testing server | 9000 (web UI), 2500 (SMTP) | +| `redis` | Cache and background job queue | 63790 | +| `minio` | S3-compatible storage | 9002 (API), 9001 (console) | +| `gotenberg` | Document conversion (optional) | 3005 | + +The local email server is [Inbucket](https://www.inbucket.org/). Open its web UI at [http://localhost:9000](http://localhost:9000) to view emails Documenso sends during development. For your own deployment you can use any SMTP-compatible mailserver, such as Inbucket, [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). ## Useful Commands diff --git a/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx b/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx index 13b2b75ab..c64bd081e 100644 --- a/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx +++ b/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx @@ -141,8 +141,8 @@ If building from source (not using Docker images): | Requirement | Version | | ----------- | ------- | -| Node.js | 18+ | -| npm | 8+ | +| Node.js | 22+ | +| npm | 11+ | --- @@ -169,7 +169,7 @@ Documenso runs on: | MySQL/MariaDB | PostgreSQL-specific features required | | SQLite | Not suitable for production workloads | | MongoDB | Relational database required | -| Node.js < 18 | Modern JavaScript features required | +| Node.js < 22 | Modern JavaScript features required | --- diff --git a/apps/docs/content/docs/self-hosting/getting-started/tips.mdx b/apps/docs/content/docs/self-hosting/getting-started/tips.mdx index 7fe640ab3..f4ff0e0a5 100644 --- a/apps/docs/content/docs/self-hosting/getting-started/tips.mdx +++ b/apps/docs/content/docs/self-hosting/getting-started/tips.mdx @@ -92,7 +92,7 @@ Use a specific version tag in production: ```bash # Good — predictable, reproducible -docker pull documenso/documenso:1.8.0 +docker pull documenso/documenso: # Risky — may pull breaking changes docker pull documenso/documenso:latest diff --git a/apps/docs/content/docs/self-hosting/index.mdx b/apps/docs/content/docs/self-hosting/index.mdx index 4263d30d4..3f8f8d3c2 100644 --- a/apps/docs/content/docs/self-hosting/index.mdx +++ b/apps/docs/content/docs/self-hosting/index.mdx @@ -27,6 +27,14 @@ import { Callout } from 'fumadocs-ui/components/callout'; Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding. + + **You are responsible for your own network security.** Documenso applies best-effort, non-exhaustive + checks to outbound requests such as webhooks, but these are not a complete SSRF mitigation and they + fail open. A self-hosted instance can reach internal addresses on your network. Restricting outbound + traffic, egress filtering, and blocking access to internal services and cloud metadata endpoints is + your responsibility through your firewall and network configuration. + + --- ## Deployment Options diff --git a/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx b/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx index 4ac7879cf..832bb0088 100644 --- a/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx +++ b/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx @@ -165,10 +165,10 @@ See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strat ### Pull the new image ```bash -docker pull documenso/documenso:1.6.0 +docker pull documenso/documenso: ``` -Replace `1.6.0` with your target version. +Replace `` with your target version. @@ -189,7 +189,7 @@ docker run -d \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ - documenso/documenso:1.6.0 + documenso/documenso: ``` @@ -223,14 +223,14 @@ Edit `compose.yml` or your `.env` file to specify the new version: ```yaml services: documenso: - image: documenso/documenso:1.6.0 + image: documenso/documenso: ``` Or if using environment variable substitution: ```bash # In .env -DOCUMENSO_VERSION=1.6.0 +DOCUMENSO_VERSION= ``` ```yaml @@ -283,7 +283,7 @@ Edit the deployment directly: ```bash kubectl set image deployment/documenso \ - documenso=documenso/documenso:1.6.0 \ + documenso=documenso/documenso: \ -n documenso ``` @@ -295,7 +295,7 @@ spec: spec: containers: - name: documenso - image: documenso/documenso:1.6.0 + image: documenso/documenso: ``` Then apply: @@ -421,12 +421,12 @@ To run migrations manually before upgrading: ```bash # Pull the new image -docker pull documenso/documenso:1.6.0 +docker pull documenso/documenso: # Run migrations only docker run --rm \ -e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \ - documenso/documenso:1.6.0 \ + documenso/documenso: \ npx prisma migrate deploy ``` @@ -516,7 +516,7 @@ docker run -d \ -p 3000:3000 \ --env-file .env \ -v /path/to/cert.p12:/opt/documenso/cert.p12:ro \ - documenso/documenso:1.5.0 + documenso/documenso: ``` diff --git a/apps/remix/Dockerfile b/apps/remix/Dockerfile deleted file mode 100644 index 207bf937e..000000000 --- a/apps/remix/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20-alpine AS development-dependencies-env -COPY . /app -WORKDIR /app -RUN npm ci - -FROM node:20-alpine AS production-dependencies-env -COPY ./package.json package-lock.json /app/ -WORKDIR /app -RUN npm ci --omit=dev - -FROM node:20-alpine AS build-env -COPY . /app/ -COPY --from=development-dependencies-env /app/node_modules /app/node_modules -WORKDIR /app -RUN npm run build - -FROM node:20-alpine -COPY ./package.json package-lock.json /app/ -COPY --from=production-dependencies-env /app/node_modules /app/node_modules -COPY --from=build-env /app/build /app/build -WORKDIR /app -CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/apps/remix/README.md b/apps/remix/README.md index e0d20664e..6824c05b1 100644 --- a/apps/remix/README.md +++ b/apps/remix/README.md @@ -1,100 +1,14 @@ -# Welcome to React Router! +# @documenso/remix -A modern, production-ready template for building full-stack React applications using React Router. +The main Documenso web application. Built with [React Router v7](https://reactrouter.com/) and served by a [Hono](https://hono.dev/) server. -[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) +This package is part of the Documenso monorepo and is not meant to be run standalone. Use the root scripts instead. -## Features - -- 🚀 Server-side rendering -- ⚡️ Hot Module Replacement (HMR) -- 📦 Asset bundling and optimization -- 🔄 Data loading and mutations -- 🔒 TypeScript by default -- 🎉 TailwindCSS for styling -- 📖 [React Router docs](https://reactrouter.com/) - -## Getting Started - -### Installation - -Install the dependencies: - -```bash -npm install -``` - -### Development - -Start the development server with HMR: +- Local development: see the [root README](../../README.md) and the [Local Development docs](https://docs.documenso.com/docs/developers/local-development). +- Self-hosting and deployment: see the [Self-Hosting docs](https://docs.documenso.com/docs/self-hosting). +- Architecture overview: see [ARCHITECTURE.md](../../ARCHITECTURE.md). ```bash +# From the monorepo root npm run dev ``` - -Your application will be available at `http://localhost:5173`. - -## Building for Production - -Create a production build: - -```bash -npm run build -``` - -## Deployment - -### Docker Deployment - -This template includes three Dockerfiles optimized for different package managers: - -- `Dockerfile` - for npm -- `Dockerfile.pnpm` - for pnpm -- `Dockerfile.bun` - for bun - -To build and run using Docker: - -```bash -# For npm -docker build -t my-app . - -# For pnpm -docker build -f Dockerfile.pnpm -t my-app . - -# For bun -docker build -f Dockerfile.bun -t my-app . - -# Run the container -docker run -p 3000:3000 my-app -``` - -The containerized application can be deployed to any platform that supports Docker, including: - -- AWS ECS -- Google Cloud Run -- Azure Container Apps -- Digital Ocean App Platform -- Fly.io -- Railway - -### DIY Deployment - -If you're familiar with deploying Node applications, the built-in app server is production-ready. - -Make sure to deploy the output of `npm run build` - -``` -├── package.json -├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) -├── build/ -│ ├── client/ # Static assets -│ └── server/ # Server-side code -``` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. - ---- - -Built with ❤️ using React Router. diff --git a/apps/remix/public/.well-known/security.txt b/apps/remix/public/.well-known/security.txt index 1a3f685e5..f96fce0f0 100644 --- a/apps/remix/public/.well-known/security.txt +++ b/apps/remix/public/.well-known/security.txt @@ -1,7 +1,14 @@ -# General Issues +# Report security vulnerabilities privately via GitHub Security Advisories (preferred). +Contact: https://github.com/documenso/documenso/security/advisories/new + +# Alternatively, report critical issues privately by email. +Contact: mailto:security@documenso.com + +# Security policy +Policy: https://github.com/documenso/documenso/security/policy + +# General (non-security) issues Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml -# Report critical issues privately to let us take appropriate action before publishing. -Contact: mailto:security@documenso.com Preferred-Languages: en -Canonical: https://documenso.com/.well-known/security.txt \ No newline at end of file +Canonical: https://documenso.com/.well-known/security.txt diff --git a/package.json b/package.json index 173c46bb1..e32496f6b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "with:env": "dotenv -e .env -e .env.local --", "reset:hard": "npm run clean && npm i && npm run prisma:generate", "precommit": "npm install && git add package.json package-lock.json", - "trigger:dev": "npm run with:env -- npx trigger-cli dev --handler-path=\"/api/jobs\"", "inngest:dev": "inngest dev -u http://localhost:3000/api/jobs", "make:version": "npm version --workspace @documenso/remix --include-workspace-root --no-git-tag-version -m \"v%s\"", "translate": "npm run translate:extract && npm run translate:compile", diff --git a/packages/lib/server-only/webhooks/assert-webhook-url.ts b/packages/lib/server-only/webhooks/assert-webhook-url.ts index 9467b5cc3..b89e3d432 100644 --- a/packages/lib/server-only/webhooks/assert-webhook-url.ts +++ b/packages/lib/server-only/webhooks/assert-webhook-url.ts @@ -71,10 +71,17 @@ const isBypassedHost = (url: string): boolean => { }; /** - * Asserts that a webhook URL does not resolve to a private or loopback - * address. Throws an AppError with WEBHOOK_INVALID_REQUEST if it does. + * Assert that a webhook URL does not point at a private/loopback address, + * checking both the literal host and its resolved DNS records. Throws an + * AppError with WEBHOOK_INVALID_REQUEST if it does. Hosts listed in + * NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS skip all checks. * - * Hosts listed in NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS skip all checks. + * This is best-effort, non-exhaustive SSRF defence, NOT a complete mitigation. + * It does not cover DNS rebinding (the resolved address can change between this + * check and the actual request), obscure IP encodings, or every IPv6 form, and + * it fails open on lookup errors/timeouts (see the catch below). Network-level + * SSRF protection (firewall/egress rules, blocking internal services and cloud + * metadata endpoints) remains the responsibility of the deployment. */ export const assertNotPrivateUrl = async ( url: string, diff --git a/packages/lib/server-only/webhooks/is-private-url.ts b/packages/lib/server-only/webhooks/is-private-url.ts index 8b345d15a..38f531ee0 100644 --- a/packages/lib/server-only/webhooks/is-private-url.ts +++ b/packages/lib/server-only/webhooks/is-private-url.ts @@ -3,10 +3,11 @@ import { z } from 'zod'; const ZIpSchema = z.string().ip(); /** - * Check whether a URL points to a known private/loopback address. + * Synchronously check whether a URL's host is a known private/loopback address + * (localhost, RFC 1918, link-local, loopback, etc.), regardless of protocol. * - * Performs a synchronous check against known private hostnames and IP ranges. - * Works regardless of the URL protocol. + * Best-effort and non-exhaustive: unrecognised or unparseable hosts return + * `false` (fail open). See `assertNotPrivateUrl` for the full SSRF caveats. */ export const isPrivateUrl = (url: string): boolean => { try { diff --git a/render.yaml b/render.yaml index 1811be308..9a29fae4d 100644 --- a/render.yaml +++ b/render.yaml @@ -10,7 +10,7 @@ services: envVars: # Node Version - key: NODE_VERSION - value: 18.17.0 + value: 22.13.0 - key: PORT value: 10000 diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index 9974f0b6a..000000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./packages/tsconfig/base.json", - "include": ["packages/**/*", "apps/**/*"] -} From 241929bb97109c03b56e96b432983737a9e3fb57 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 26 Jun 2026 00:01:55 +1000 Subject: [PATCH 04/41] feat: add API endpoints for downloading certificate and audit log PDFs (#3025) Add V2 API routes to download an envelope's certificate and audit log separately, and align the internal cert/audit log downloads to use envelopeId. Enforces document visibility via getEnvelopeWhereInput and loads field signatures so certificates render correctly. --- .../document-audit-log-download-button.tsx | 6 +- .../document-certificate-download-button.tsx | 6 +- .../t.$teamUrl+/documents.$id.logs.tsx | 4 +- apps/remix/server/api/download/download.ts | 252 +++++++++++++++--- .../server/api/download/download.types.ts | 14 + .../download-document-audit-logs.ts | 12 +- .../download-document-audit-logs.types.ts | 2 +- .../download-document-certificate.ts | 12 +- .../download-document-certificate.types.ts | 2 +- .../download-envelope-audit-log-pdf.ts | 23 ++ .../download-envelope-audit-log-pdf.types.ts | 25 ++ .../download-envelope-certificate-pdf.ts | 23 ++ ...download-envelope-certificate-pdf.types.ts | 25 ++ .../trpc/server/envelope-router/router.ts | 6 + 14 files changed, 348 insertions(+), 64 deletions(-) create mode 100644 packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.ts create mode 100644 packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts create mode 100644 packages/trpc/server/envelope-router/download-envelope-certificate-pdf.ts create mode 100644 packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts diff --git a/apps/remix/app/components/general/document/document-audit-log-download-button.tsx b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx index 109c3b9f4..695b31364 100644 --- a/apps/remix/app/components/general/document/document-audit-log-download-button.tsx +++ b/apps/remix/app/components/general/document/document-audit-log-download-button.tsx @@ -11,10 +11,10 @@ import { DownloadIcon } from 'lucide-react'; export type DocumentAuditLogDownloadButtonProps = { className?: string; - documentId: number; + envelopeId: string; }; -export const DocumentAuditLogDownloadButton = ({ className, documentId }: DocumentAuditLogDownloadButtonProps) => { +export const DocumentAuditLogDownloadButton = ({ className, envelopeId }: DocumentAuditLogDownloadButtonProps) => { const { toast } = useToast(); const { _ } = useLingui(); @@ -22,7 +22,7 @@ export const DocumentAuditLogDownloadButton = ({ className, documentId }: Docume const onDownloadAuditLogsClick = async () => { try { - const { data, envelopeTitle } = await downloadAuditLogs({ documentId }); + const { data, envelopeTitle } = await downloadAuditLogs({ envelopeId }); const buffer = new Uint8Array(base64.decode(data)); const blob = new Blob([buffer], { type: 'application/pdf' }); diff --git a/apps/remix/app/components/general/document/document-certificate-download-button.tsx b/apps/remix/app/components/general/document/document-certificate-download-button.tsx index a862ada7f..f75b41dd9 100644 --- a/apps/remix/app/components/general/document/document-certificate-download-button.tsx +++ b/apps/remix/app/components/general/document/document-certificate-download-button.tsx @@ -13,13 +13,13 @@ import { DownloadIcon } from 'lucide-react'; export type DocumentCertificateDownloadButtonProps = { className?: string; - documentId: number; + envelopeId: string; documentStatus: DocumentStatus; }; export const DocumentCertificateDownloadButton = ({ className, - documentId, + envelopeId, documentStatus, }: DocumentCertificateDownloadButtonProps) => { const { toast } = useToast(); @@ -29,7 +29,7 @@ export const DocumentCertificateDownloadButton = ({ const onDownloadCertificatesClick = async () => { try { - const { data, envelopeTitle } = await downloadCertificate({ documentId }); + const { data, envelopeTitle } = await downloadCertificate({ envelopeId }); const buffer = new Uint8Array(base64.decode(data)); const blob = new Blob([buffer], { type: 'application/pdf' }); diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx index 2ed2ca142..9b8045bc0 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx @@ -153,11 +153,11 @@ export default function DocumentsLogsPage({ loaderData }: Route.ComponentProps)
- +
diff --git a/apps/remix/server/api/download/download.ts b/apps/remix/server/api/download/download.ts index 012d9e875..796a68653 100644 --- a/apps/remix/server/api/download/download.ts +++ b/apps/remix/server/api/download/download.ts @@ -1,20 +1,52 @@ +import { PDF_SIZE_A4_72PPI } from '@documenso/lib/constants/pdf'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; +import { getEnvelopeById, getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; +import { generateAuditLogPdf } from '@documenso/lib/server-only/pdf/generate-audit-log-pdf'; +import { generateCertificatePdf } from '@documenso/lib/server-only/pdf/generate-certificate-pdf'; import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token'; +import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; import { sValidator } from '@hono/standard-validator'; -import { EnvelopeType } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; +import contentDisposition from 'content-disposition'; import { Hono } from 'hono'; import type { HonoEnv } from '../../router'; import { handleEnvelopeItemFileRequest } from '../files/files.helpers'; import { ZDownloadDocumentRequestParamsSchema, + ZDownloadEnvelopeAuditLogPdfRequestParamsSchema, + ZDownloadEnvelopeCertificatePdfRequestParamsSchema, ZDownloadEnvelopeItemRequestParamsSchema, ZDownloadEnvelopeItemRequestQuerySchema, } from './download.types'; +/** + * Resolve and validate an API token from the Authorization header. + * + * Supports both "Authorization: Bearer api_xxx" and "Authorization: api_xxx". + */ +const resolveApiToken = async (authorizationHeader: string | undefined) => { + const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); + + if (!token) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'API token was not provided', + }); + } + + const apiToken = await getApiTokenByToken({ token }); + + if (apiToken.user.disabled) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'User is disabled', + }); + } + + return apiToken; +}; + export const downloadRoute = new Hono() /** * Download an envelope item by its ID. @@ -30,24 +62,8 @@ export const downloadRoute = new Hono() try { const { envelopeItemId } = c.req.valid('param'); const { version } = c.req.valid('query'); - const authorizationHeader = c.req.header('authorization'); - // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx" - const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); - - if (!token) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'API token was not provided', - }); - } - - const apiToken = await getApiTokenByToken({ token }); - - if (apiToken.user.disabled) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'User is disabled', - }); - } + const apiToken = await resolveApiToken(c.req.header('authorization')); logger.info({ auth: 'api', @@ -125,6 +141,180 @@ export const downloadRoute = new Hono() } }, ) + /** + * Download the audit log for a document as a PDF. + * Requires API key authentication via Authorization header. + */ + .get( + '/envelope/:envelopeId/audit-log/pdf', + sValidator('param', ZDownloadEnvelopeAuditLogPdfRequestParamsSchema), + async (c) => { + const logger = c.get('logger'); + + try { + const { envelopeId } = c.req.valid('param'); + + const apiToken = await resolveApiToken(c.req.header('authorization')); + + logger.info({ + auth: 'api', + source: 'apiV2', + path: c.req.path, + userId: apiToken.user.id, + apiTokenId: apiToken.id, + envelopeId, + }); + + const envelope = await getEnvelopeById({ + id: { + type: 'envelopeId', + id: envelopeId, + }, + type: EnvelopeType.DOCUMENT, + userId: apiToken.user.id, + teamId: apiToken.teamId, + }).catch(() => null); + + if (!envelope) { + return c.json({ error: 'Document not found' }, 404); + } + + const auditLogPdf = await generateAuditLogPdf({ + envelope, + recipients: envelope.recipients, + fields: envelope.fields, + language: envelope.documentMeta.language, + envelopeOwner: { + email: envelope.user.email, + name: envelope.user.name || '', + }, + envelopeItems: envelope.envelopeItems.map((item) => item.title), + pageWidth: PDF_SIZE_A4_72PPI.width, + pageHeight: PDF_SIZE_A4_72PPI.height, + }); + + const result = await auditLogPdf.save(); + + const baseTitle = envelope.title.replace(/\.pdf$/i, ''); + + c.header('Content-Type', 'application/pdf'); + c.header('Content-Disposition', contentDisposition(`${baseTitle}_audit-log.pdf`)); + c.header('Cache-Control', 'no-cache, no-store, must-revalidate'); + + return c.body(result); + } catch (error) { + logger.error(error); + + if (error instanceof AppError) { + const { status, body } = AppError.toRestAPIError(error); + + return c.json({ error: body.message, code: error.code }, status); + } + + return c.json({ error: 'Internal server error' }, 500); + } + }, + ) + /** + * Download the signing certificate for a completed document as a PDF. + * Requires API key authentication via Authorization header. + */ + .get( + '/envelope/:envelopeId/certificate/pdf', + sValidator('param', ZDownloadEnvelopeCertificatePdfRequestParamsSchema), + async (c) => { + const logger = c.get('logger'); + + try { + const { envelopeId } = c.req.valid('param'); + + const apiToken = await resolveApiToken(c.req.header('authorization')); + + logger.info({ + auth: 'api', + source: 'apiV2', + path: c.req.path, + userId: apiToken.user.id, + apiTokenId: apiToken.id, + envelopeId, + }); + + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: envelopeId, + }, + type: EnvelopeType.DOCUMENT, + userId: apiToken.user.id, + teamId: apiToken.teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + include: { + recipients: true, + fields: { + include: { + signature: true, + }, + }, + documentMeta: true, + user: { + select: { + email: true, + name: true, + }, + }, + }, + }); + + if (!envelope) { + return c.json({ error: 'Document not found' }, 404); + } + + // A cancelled document was never sealed/completed, so a signing certificate + // must not be generated for it. + if (!isDocumentCompleted(envelope.status) || envelope.status === DocumentStatus.CANCELLED) { + throw new AppError('DOCUMENT_NOT_COMPLETE', { + message: 'Document is not complete', + }); + } + + const certificatePdf = await generateCertificatePdf({ + envelope, + recipients: envelope.recipients, + fields: envelope.fields, + language: envelope.documentMeta.language, + envelopeOwner: { + email: envelope.user.email, + name: envelope.user.name || '', + }, + pageWidth: PDF_SIZE_A4_72PPI.width, + pageHeight: PDF_SIZE_A4_72PPI.height, + }); + + const result = await certificatePdf.save(); + + const baseTitle = envelope.title.replace(/\.pdf$/i, ''); + + c.header('Content-Type', 'application/pdf'); + c.header('Content-Disposition', contentDisposition(`${baseTitle}_certificate.pdf`)); + c.header('Cache-Control', 'no-cache, no-store, must-revalidate'); + + return c.body(result); + } catch (error) { + logger.error(error); + + if (error instanceof AppError) { + const { status, body } = AppError.toRestAPIError(error); + + return c.json({ error: body.message, code: error.code }, status); + } + + return c.json({ error: 'Internal server error' }, 500); + } + }, + ) /** * Download a document by its ID. * Requires API key authentication via Authorization header. @@ -134,24 +324,8 @@ export const downloadRoute = new Hono() try { const { documentId, version } = c.req.valid('param'); - const authorizationHeader = c.req.header('authorization'); - // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx" - const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); - - if (!token) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'API token was not provided', - }); - } - - const apiToken = await getApiTokenByToken({ token }); - - if (apiToken.user.disabled) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'User is disabled', - }); - } + const apiToken = await resolveApiToken(c.req.header('authorization')); logger.info({ auth: 'api', @@ -200,11 +374,9 @@ export const downloadRoute = new Hono() logger.error(error); if (error instanceof AppError) { - if (error.code === AppErrorCode.UNAUTHORIZED) { - return c.json({ error: error.message }, 401); - } + const { status, body } = AppError.toRestAPIError(error); - return c.json({ error: error.message }, 400); + return c.json({ error: body.message, code: error.code }, status); } return c.json({ error: 'Internal server error' }, 500); diff --git a/apps/remix/server/api/download/download.types.ts b/apps/remix/server/api/download/download.types.ts index 86a827d31..d5a64d468 100644 --- a/apps/remix/server/api/download/download.types.ts +++ b/apps/remix/server/api/download/download.types.ts @@ -30,3 +30,17 @@ export const ZDownloadDocumentRequestParamsSchema = z.object({ }); export type TDownloadDocumentRequestParams = z.infer; + +export const ZDownloadEnvelopeAuditLogPdfRequestParamsSchema = z.object({ + envelopeId: z.string().describe('The ID of the envelope to download the audit log for.'), +}); + +export type TDownloadEnvelopeAuditLogPdfRequestParams = z.infer; + +export const ZDownloadEnvelopeCertificatePdfRequestParamsSchema = z.object({ + envelopeId: z.string().describe('The ID of the envelope to download the certificate for.'), +}); + +export type TDownloadEnvelopeCertificatePdfRequestParams = z.infer< + typeof ZDownloadEnvelopeCertificatePdfRequestParamsSchema +>; diff --git a/packages/trpc/server/document-router/download-document-audit-logs.ts b/packages/trpc/server/document-router/download-document-audit-logs.ts index a2dfc0731..398fbfb6c 100644 --- a/packages/trpc/server/document-router/download-document-audit-logs.ts +++ b/packages/trpc/server/document-router/download-document-audit-logs.ts @@ -15,18 +15,18 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure .output(ZDownloadDocumentAuditLogsResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; - const { documentId } = input; + const { envelopeId } = input; ctx.logger.info({ input: { - documentId, + envelopeId, }, }); const envelope = await getEnvelopeById({ id: { - type: 'documentId', - id: documentId, + type: 'envelopeId', + id: envelopeId, }, type: EnvelopeType.DOCUMENT, userId: ctx.user.id, @@ -55,10 +55,8 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure const result = await certificatePdf.save(); - const base64 = Buffer.from(result).toString('base64'); - return { - data: base64, + data: Buffer.from(result).toString('base64'), envelopeTitle: envelope.title, }; }); diff --git a/packages/trpc/server/document-router/download-document-audit-logs.types.ts b/packages/trpc/server/document-router/download-document-audit-logs.types.ts index a06825f2e..e5766b1ec 100644 --- a/packages/trpc/server/document-router/download-document-audit-logs.types.ts +++ b/packages/trpc/server/document-router/download-document-audit-logs.types.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const ZDownloadDocumentAuditLogsRequestSchema = z.object({ - documentId: z.number(), + envelopeId: z.string(), }); export const ZDownloadDocumentAuditLogsResponseSchema = z.object({ diff --git a/packages/trpc/server/document-router/download-document-certificate.ts b/packages/trpc/server/document-router/download-document-certificate.ts index 749afe50e..5180af962 100644 --- a/packages/trpc/server/document-router/download-document-certificate.ts +++ b/packages/trpc/server/document-router/download-document-certificate.ts @@ -17,18 +17,18 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure .output(ZDownloadDocumentCertificateResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId } = ctx; - const { documentId } = input; + const { envelopeId } = input; ctx.logger.info({ input: { - documentId, + envelopeId, }, }); const { envelopeWhereInput } = await getEnvelopeWhereInput({ id: { - type: 'documentId', - id: documentId, + type: 'envelopeId', + id: envelopeId, }, type: EnvelopeType.DOCUMENT, userId: ctx.user.id, @@ -81,10 +81,8 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure const result = await certificatePdf.save(); - const base64 = Buffer.from(result).toString('base64'); - return { - data: base64, + data: Buffer.from(result).toString('base64'), envelopeTitle: envelope.title, }; }); diff --git a/packages/trpc/server/document-router/download-document-certificate.types.ts b/packages/trpc/server/document-router/download-document-certificate.types.ts index ca8107cc8..72bdd3e6a 100644 --- a/packages/trpc/server/document-router/download-document-certificate.types.ts +++ b/packages/trpc/server/document-router/download-document-certificate.types.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const ZDownloadDocumentCertificateRequestSchema = z.object({ - documentId: z.number(), + envelopeId: z.string(), }); export const ZDownloadDocumentCertificateResponseSchema = z.object({ diff --git a/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.ts b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.ts new file mode 100644 index 000000000..3bd8fc74a --- /dev/null +++ b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.ts @@ -0,0 +1,23 @@ +import { authenticatedProcedure } from '../trpc'; +import { + downloadEnvelopeAuditLogPdfMeta, + ZDownloadEnvelopeAuditLogPdfRequestSchema, + ZDownloadEnvelopeAuditLogPdfResponseSchema, +} from './download-envelope-audit-log-pdf.types'; + +export const downloadEnvelopeAuditLogPdfRoute = authenticatedProcedure + .meta(downloadEnvelopeAuditLogPdfMeta) + .input(ZDownloadEnvelopeAuditLogPdfRequestSchema) + .output(ZDownloadEnvelopeAuditLogPdfResponseSchema) + .query(({ input, ctx }) => { + const { envelopeId } = input; + + ctx.logger.info({ + input: { + envelopeId, + }, + }); + + // This endpoint is purely for V2 API, which is implemented in the Hono remix server. + throw new Error('NOT_IMPLEMENTED'); + }); diff --git a/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts new file mode 100644 index 000000000..3e43033ac --- /dev/null +++ b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import type { TrpcRouteMeta } from '../trpc'; + +export const downloadEnvelopeAuditLogPdfMeta: TrpcRouteMeta = { + openapi: { + method: 'GET', + path: '/envelope/{envelopeId}/audit-log/pdf', + summary: 'Download envelope audit log PDF', + description: 'Download the audit log for a document as a PDF.', + tags: ['Envelope'], + responseHeaders: z.object({ + 'Content-Type': z.literal('application/pdf'), + }), + }, +}; + +export const ZDownloadEnvelopeAuditLogPdfRequestSchema = z.object({ + envelopeId: z.string().describe('The ID of the envelope to download the audit log for.'), +}); + +export const ZDownloadEnvelopeAuditLogPdfResponseSchema = z.instanceof(Uint8Array); + +export type TDownloadEnvelopeAuditLogPdfRequest = z.infer; +export type TDownloadEnvelopeAuditLogPdfResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.ts b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.ts new file mode 100644 index 000000000..9fe430bb4 --- /dev/null +++ b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.ts @@ -0,0 +1,23 @@ +import { authenticatedProcedure } from '../trpc'; +import { + downloadEnvelopeCertificatePdfMeta, + ZDownloadEnvelopeCertificatePdfRequestSchema, + ZDownloadEnvelopeCertificatePdfResponseSchema, +} from './download-envelope-certificate-pdf.types'; + +export const downloadEnvelopeCertificatePdfRoute = authenticatedProcedure + .meta(downloadEnvelopeCertificatePdfMeta) + .input(ZDownloadEnvelopeCertificatePdfRequestSchema) + .output(ZDownloadEnvelopeCertificatePdfResponseSchema) + .query(({ input, ctx }) => { + const { envelopeId } = input; + + ctx.logger.info({ + input: { + envelopeId, + }, + }); + + // This endpoint is purely for V2 API, which is implemented in the Hono remix server. + throw new Error('NOT_IMPLEMENTED'); + }); diff --git a/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts new file mode 100644 index 000000000..eeab2be69 --- /dev/null +++ b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +import type { TrpcRouteMeta } from '../trpc'; + +export const downloadEnvelopeCertificatePdfMeta: TrpcRouteMeta = { + openapi: { + method: 'GET', + path: '/envelope/{envelopeId}/certificate/pdf', + summary: 'Download envelope certificate PDF', + description: 'Download the signing certificate for a completed document as a PDF.', + tags: ['Envelope'], + responseHeaders: z.object({ + 'Content-Type': z.literal('application/pdf'), + }), + }, +}; + +export const ZDownloadEnvelopeCertificatePdfRequestSchema = z.object({ + envelopeId: z.string().describe('The ID of the envelope to download the certificate for.'), +}); + +export const ZDownloadEnvelopeCertificatePdfResponseSchema = z.instanceof(Uint8Array); + +export type TDownloadEnvelopeCertificatePdfRequest = z.infer; +export type TDownloadEnvelopeCertificatePdfResponse = z.infer; diff --git a/packages/trpc/server/envelope-router/router.ts b/packages/trpc/server/envelope-router/router.ts index 1dd54e436..c6e34dab1 100644 --- a/packages/trpc/server/envelope-router/router.ts +++ b/packages/trpc/server/envelope-router/router.ts @@ -12,6 +12,8 @@ import { createEnvelopeItemsRoute } from './create-envelope-items'; import { deleteEnvelopeRoute } from './delete-envelope'; import { deleteEnvelopeItemRoute } from './delete-envelope-item'; import { distributeEnvelopeRoute } from './distribute-envelope'; +import { downloadEnvelopeAuditLogPdfRoute } from './download-envelope-audit-log-pdf'; +import { downloadEnvelopeCertificatePdfRoute } from './download-envelope-certificate-pdf'; import { downloadEnvelopeItemRoute } from './download-envelope-item'; import { duplicateEnvelopeRoute } from './duplicate-envelope'; import { createEnvelopeFieldsRoute } from './envelope-fields/create-envelope-fields'; @@ -85,6 +87,10 @@ export const envelopeRouter = router({ find: findEnvelopesRoute, auditLog: { find: findEnvelopeAuditLogsRoute, + downloadPdf: downloadEnvelopeAuditLogPdfRoute, + }, + certificate: { + downloadPdf: downloadEnvelopeCertificatePdfRoute, }, bulk: { move: bulkMoveEnvelopesRoute, From 96ab78c33f617e43db1a8ba84396f584e815fc8e Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Fri, 26 Jun 2026 14:46:30 +1000 Subject: [PATCH 05/41] fix: resolve permission issues (#3029) --- .../organisation-permission-hierarchy.spec.ts | 342 ++++++++++++++++++ .../delete-organisation-group.ts | 19 +- .../delete-organisation-members.ts | 67 +++- .../organisation-router/leave-organisation.ts | 8 + .../resend-organisation-member-invite.ts | 18 +- 5 files changed, 447 insertions(+), 7 deletions(-) create mode 100644 packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts diff --git a/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts b/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts new file mode 100644 index 000000000..b688c7cd9 --- /dev/null +++ b/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts @@ -0,0 +1,342 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { generateDatabaseId, nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { seedOrganisationMembers } from '@documenso/prisma/seed/organisations'; +import { seedUser } from '@documenso/prisma/seed/users'; +import { expect, type Page, test } from '@playwright/test'; +import { OrganisationGroupType, type OrganisationMemberRole } from '@prisma/client'; + +import { apiSignin } from '../fixtures/authentication'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); + +/** + * Calls a tRPC mutation directly using the cookies of whoever is currently + * signed in on the page context. This deliberately bypasses the UI: the + * authorisation checks under test live on the server, and the UI may simply + * hide a button rather than reject the request, which would mask a backend gap. + */ +const trpcMutation = async (page: Page, procedure: string, input: Record) => { + return await page.request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, { + headers: { 'content-type': 'application/json' }, + data: JSON.stringify({ json: input }), + }); +}; + +const getOrganisationMember = async (userId: number, organisationId: string) => { + return await prisma.organisationMember.findFirstOrThrow({ + where: { + userId, + organisationId, + }, + }); +}; + +const createCustomGroup = async (organisationId: string, organisationRole: OrganisationMemberRole) => { + return await prisma.organisationGroup.create({ + data: { + id: generateDatabaseId('org_group'), + organisationId, + name: `custom-${organisationRole}-${nanoid()}`, + type: OrganisationGroupType.CUSTOM, + organisationRole, + }, + }); +}; + +const createPendingInvite = async (organisationId: string, organisationRole: OrganisationMemberRole) => { + return await prisma.organisationMemberInvite.create({ + data: { + id: generateDatabaseId('member_invite'), + email: `invite-${nanoid()}@test.documenso.com`, + token: nanoid(32), + organisationId, + organisationRole, + }, + }); +}; + +test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: member deletion', () => { + test('a manager cannot delete an admin via member.delete', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser, adminUser] = await seedOrganisationMembers({ + members: [ + { name: 'Manager', organisationRole: 'MANAGER' }, + { name: 'Admin', organisationRole: 'ADMIN' }, + ], + organisationId: organisation.id, + }); + + const adminMember = await getOrganisationMember(adminUser.id, organisation.id); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.delete', { + organisationId: organisation.id, + organisationMemberId: adminMember.id, + }); + + expect(res.ok()).toBeFalsy(); + + // The admin must still be a member of the organisation. + const stillExists = await prisma.organisationMember.findFirst({ + where: { id: adminMember.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('a manager cannot delete an admin via member.deleteMany', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser, adminUser] = await seedOrganisationMembers({ + members: [ + { name: 'Manager', organisationRole: 'MANAGER' }, + { name: 'Admin', organisationRole: 'ADMIN' }, + ], + organisationId: organisation.id, + }); + + const adminMember = await getOrganisationMember(adminUser.id, organisation.id); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.deleteMany', { + organisationId: organisation.id, + organisationMemberIds: [adminMember.id], + }); + + expect(res.ok()).toBeFalsy(); + + const stillExists = await prisma.organisationMember.findFirst({ + where: { id: adminMember.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('a manager cannot delete the organisation owner', async ({ page }) => { + const { user: ownerUser, organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser] = await seedOrganisationMembers({ + members: [{ name: 'Manager', organisationRole: 'MANAGER' }], + organisationId: organisation.id, + }); + + const ownerMember = await getOrganisationMember(ownerUser.id, organisation.id); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.deleteMany', { + organisationId: organisation.id, + organisationMemberIds: [ownerMember.id], + }); + + expect(res.ok()).toBeFalsy(); + + const stillExists = await prisma.organisationMember.findFirst({ + where: { id: ownerMember.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('an admin cannot delete the organisation owner', async ({ page }) => { + const { user: ownerUser, organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [adminUser] = await seedOrganisationMembers({ + members: [{ name: 'Admin', organisationRole: 'ADMIN' }], + organisationId: organisation.id, + }); + + const ownerMember = await getOrganisationMember(ownerUser.id, organisation.id); + + await apiSignin({ page, email: adminUser.email }); + + const res = await trpcMutation(page, 'organisation.member.deleteMany', { + organisationId: organisation.id, + organisationMemberIds: [ownerMember.id], + }); + + expect(res.ok()).toBeFalsy(); + + const stillExists = await prisma.organisationMember.findFirst({ + where: { id: ownerMember.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('a manager can still delete a regular member (positive control)', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser, memberUser] = await seedOrganisationMembers({ + members: [ + { name: 'Manager', organisationRole: 'MANAGER' }, + { name: 'Member', organisationRole: 'MEMBER' }, + ], + organisationId: organisation.id, + }); + + const member = await getOrganisationMember(memberUser.id, organisation.id); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.deleteMany', { + organisationId: organisation.id, + organisationMemberIds: [member.id], + }); + + expect(res.ok()).toBeTruthy(); + + const deleted = await prisma.organisationMember.findFirst({ + where: { id: member.id }, + }); + + expect(deleted).toBeNull(); + }); +}); + +test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: group deletion', () => { + test('a manager cannot delete an admin-role group', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser] = await seedOrganisationMembers({ + members: [{ name: 'Manager', organisationRole: 'MANAGER' }], + organisationId: organisation.id, + }); + + const adminGroup = await createCustomGroup(organisation.id, 'ADMIN'); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.group.delete', { + organisationId: organisation.id, + groupId: adminGroup.id, + }); + + expect(res.ok()).toBeFalsy(); + + const stillExists = await prisma.organisationGroup.findFirst({ + where: { id: adminGroup.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('a manager can delete a member-role group (positive control)', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser] = await seedOrganisationMembers({ + members: [{ name: 'Manager', organisationRole: 'MANAGER' }], + organisationId: organisation.id, + }); + + const memberGroup = await createCustomGroup(organisation.id, 'MEMBER'); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.group.delete', { + organisationId: organisation.id, + groupId: memberGroup.id, + }); + + expect(res.ok()).toBeTruthy(); + + const deleted = await prisma.organisationGroup.findFirst({ + where: { id: memberGroup.id }, + }); + + expect(deleted).toBeNull(); + }); +}); + +test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: invite resend', () => { + test('a manager cannot resend an admin-role invite', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser] = await seedOrganisationMembers({ + members: [{ name: 'Manager', organisationRole: 'MANAGER' }], + organisationId: organisation.id, + }); + + const adminInvite = await createPendingInvite(organisation.id, 'ADMIN'); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.invite.resend', { + organisationId: organisation.id, + invitationId: adminInvite.id, + }); + + expect(res.ok()).toBeFalsy(); + }); + + test('a manager can resend a member-role invite (positive control)', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [managerUser] = await seedOrganisationMembers({ + members: [{ name: 'Manager', organisationRole: 'MANAGER' }], + organisationId: organisation.id, + }); + + const memberInvite = await createPendingInvite(organisation.id, 'MEMBER'); + + await apiSignin({ page, email: managerUser.email }); + + const res = await trpcMutation(page, 'organisation.member.invite.resend', { + organisationId: organisation.id, + invitationId: memberInvite.id, + }); + + expect(res.ok()).toBeTruthy(); + }); +}); + +test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: leaving an organisation', () => { + test('the owner cannot leave without transferring ownership first', async ({ page }) => { + const { user: ownerUser, organisation } = await seedUser({ isPersonalOrganisation: false }); + + const ownerMember = await getOrganisationMember(ownerUser.id, organisation.id); + + await apiSignin({ page, email: ownerUser.email }); + + const res = await trpcMutation(page, 'organisation.leave', { + organisationId: organisation.id, + }); + + expect(res.ok()).toBeFalsy(); + + const stillExists = await prisma.organisationMember.findFirst({ + where: { id: ownerMember.id }, + }); + + expect(stillExists).not.toBeNull(); + }); + + test('a non-owner member can still leave (positive control)', async ({ page }) => { + const { organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [memberUser] = await seedOrganisationMembers({ + members: [{ name: 'Member', organisationRole: 'MEMBER' }], + organisationId: organisation.id, + }); + + const member = await getOrganisationMember(memberUser.id, organisation.id); + + await apiSignin({ page, email: memberUser.email }); + + const res = await trpcMutation(page, 'organisation.leave', { + organisationId: organisation.id, + }); + + expect(res.ok()).toBeTruthy(); + + const deleted = await prisma.organisationMember.findFirst({ + where: { id: member.id }, + }); + + expect(deleted).toBeNull(); + }); +}); diff --git a/packages/trpc/server/organisation-router/delete-organisation-group.ts b/packages/trpc/server/organisation-router/delete-organisation-group.ts index b1230ab34..33bac9dc6 100644 --- a/packages/trpc/server/organisation-router/delete-organisation-group.ts +++ b/packages/trpc/server/organisation-router/delete-organisation-group.ts @@ -1,6 +1,7 @@ import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations'; +import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles'; +import { buildOrganisationWhereQuery, isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations'; import { prisma } from '@documenso/prisma'; import { OrganisationGroupType } from '@prisma/client'; @@ -59,6 +60,22 @@ export const deleteOrganisationGroupRoute = authenticatedProcedure }); } + const currentUserOrganisationRole = await getMemberOrganisationRole({ + organisationId, + reference: { + type: 'User', + id: user.id, + }, + }); + + // A user cannot delete a group whose role is higher than their own + // (e.g. a manager deleting an admin-role group). + if (!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, group.organisationRole)) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You are not allowed to delete this organisation group', + }); + } + await prisma.organisationGroup.delete({ where: { id: groupId, diff --git a/packages/trpc/server/organisation-router/delete-organisation-members.ts b/packages/trpc/server/organisation-router/delete-organisation-members.ts index dd5822265..d19e3b71b 100644 --- a/packages/trpc/server/organisation-router/delete-organisation-members.ts +++ b/packages/trpc/server/organisation-router/delete-organisation-members.ts @@ -1,8 +1,15 @@ import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/stripe/update-subscription-item-quantity'; -import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations'; +import { + ORGANISATION_MEMBER_ROLE_HIERARCHY, + ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP, +} from '@documenso/lib/constants/organisations'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { jobs } from '@documenso/lib/jobs/client'; -import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations'; +import { + buildOrganisationWhereQuery, + getHighestOrganisationRoleInGroup, + isOrganisationRoleWithinUserHierarchy, +} from '@documenso/lib/utils/organisations'; import { prisma } from '@documenso/prisma'; import { OrganisationMemberInviteStatus } from '@documenso/prisma/client'; @@ -60,9 +67,12 @@ export const deleteOrganisationMembers = async ({ }, }, members: { - select: { - id: true, - userId: true, + include: { + organisationGroupMembers: { + include: { + group: true, + }, + }, }, }, invites: { @@ -84,6 +94,41 @@ export const deleteOrganisationMembers = async ({ const membersToDelete = organisation.members.filter((member) => organisationMemberIds.includes(member.id)); + const currentUserMember = organisation.members.find((member) => member.userId === userId); + + if (!currentUserMember) { + throw new AppError(AppErrorCode.UNAUTHORIZED); + } + + const currentUserOrganisationRole = getHighestOrganisationRoleInGroup( + currentUserMember.organisationGroupMembers.map(({ group }) => group), + ); + + // The roles the current user is allowed to act on (their own role and below). + const manageableOrganisationRoles = ORGANISATION_MEMBER_ROLE_HIERARCHY[currentUserOrganisationRole]; + + for (const member of membersToDelete) { + // The organisation owner can never be removed via this route. Ownership must + // be transferred first (mirrors the admin and update-member routes). + if (member.userId === organisation.ownerUserId) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Cannot remove the organisation owner', + }); + } + + const memberOrganisationRole = getHighestOrganisationRoleInGroup( + member.organisationGroupMembers.map(({ group }) => group), + ); + + // A user cannot remove a member whose role is higher than their own + // (e.g. a manager removing an admin). + if (!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, memberOrganisationRole)) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Cannot remove a member with a higher role', + }); + } + } + const inviteCount = organisation.invites.length; const newMemberCount = organisation.members.length + inviteCount - membersToDelete.length; @@ -123,6 +168,18 @@ export const deleteOrganisationMembers = async ({ in: organisationMemberIds, }, organisationId, + userId: { + not: organisation.ownerUserId, + }, + organisationGroupMembers: { + none: { + group: { + organisationRole: { + notIn: manageableOrganisationRoles, + }, + }, + }, + }, }, }); }); diff --git a/packages/trpc/server/organisation-router/leave-organisation.ts b/packages/trpc/server/organisation-router/leave-organisation.ts index e04f5d26f..8e2b8b775 100644 --- a/packages/trpc/server/organisation-router/leave-organisation.ts +++ b/packages/trpc/server/organisation-router/leave-organisation.ts @@ -51,6 +51,14 @@ export const leaveOrganisationRoute = authenticatedProcedure throw new AppError(AppErrorCode.NOT_FOUND); } + // The organisation owner cannot leave their own organisation. Ownership must + // be transferred to another member first. + if (organisation.ownerUserId === userId) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You cannot leave an organisation you own. Please transfer ownership first.', + }); + } + const { organisationClaim } = organisation; const inviteCount = organisation.invites.length; diff --git a/packages/trpc/server/organisation-router/resend-organisation-member-invite.ts b/packages/trpc/server/organisation-router/resend-organisation-member-invite.ts index b3c2bdc68..e83c1658a 100644 --- a/packages/trpc/server/organisation-router/resend-organisation-member-invite.ts +++ b/packages/trpc/server/organisation-router/resend-organisation-member-invite.ts @@ -1,7 +1,8 @@ import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { sendOrganisationMemberInviteEmail } from '@documenso/lib/server-only/organisation/create-organisation-member-invites'; -import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations'; +import { getMemberOrganisationRole } from '@documenso/lib/server-only/team/get-member-roles'; +import { buildOrganisationWhereQuery, isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations'; import { prisma } from '@documenso/prisma'; import { authenticatedProcedure } from '../trpc'; @@ -97,6 +98,21 @@ export const resendOrganisationMemberInvitation = async ({ }); } + const currentUserOrganisationRole = await getMemberOrganisationRole({ + organisationId: organisation.id, + reference: { + type: 'User', + id: userId, + }, + }); + + // A user cannot interact with an invitation that is not within their own hierarchy. + if (!isOrganisationRoleWithinUserHierarchy(currentUserOrganisationRole, invitation.organisationRole)) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You cannot resend an invite for a member with a higher role', + }); + } + await sendOrganisationMemberInviteEmail({ email: invitation.email, token: invitation.token, From c219305eb1b60a9df4232b602434045318246344 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sat, 27 Jun 2026 19:54:14 +1000 Subject: [PATCH 06/41] chore: add tests for cert and audit log download via api (#3043) --- apps/remix/server/api/download/download.ts | 4 +- ...api-access-envelope-cert-audit-log.spec.ts | 284 ++++++++++++++++++ .../download-envelope-audit-log-pdf.types.ts | 2 +- ...download-envelope-certificate-pdf.types.ts | 2 +- 4 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cert-audit-log.spec.ts diff --git a/apps/remix/server/api/download/download.ts b/apps/remix/server/api/download/download.ts index 796a68653..6682c327c 100644 --- a/apps/remix/server/api/download/download.ts +++ b/apps/remix/server/api/download/download.ts @@ -146,7 +146,7 @@ export const downloadRoute = new Hono() * Requires API key authentication via Authorization header. */ .get( - '/envelope/:envelopeId/audit-log/pdf', + '/envelope/:envelopeId/audit-log/download', sValidator('param', ZDownloadEnvelopeAuditLogPdfRequestParamsSchema), async (c) => { const logger = c.get('logger'); @@ -220,7 +220,7 @@ export const downloadRoute = new Hono() * Requires API key authentication via Authorization header. */ .get( - '/envelope/:envelopeId/certificate/pdf', + '/envelope/:envelopeId/certificate/download', sValidator('param', ZDownloadEnvelopeCertificatePdfRequestParamsSchema), async (c) => { const logger = c.get('logger'); diff --git a/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cert-audit-log.spec.ts b/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cert-audit-log.spec.ts new file mode 100644 index 000000000..17216ee67 --- /dev/null +++ b/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cert-audit-log.spec.ts @@ -0,0 +1,284 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { hashString } from '@documenso/lib/server-only/auth/hash'; +import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; +import { alphaid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client'; +import { seedCompletedDocument } from '@documenso/prisma/seed/documents'; +import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; +import { seedUser } from '@documenso/prisma/seed/users'; +import { type APIRequestContext, expect, test } from '@playwright/test'; +import type { Team, User } from '@prisma/client'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); +const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`; + +/** + * Create an API token directly, bypassing the role check in `createApiToken`. + * + * This simulates a token that was minted while the user had permission, and which + * survives a later downgrade to a lower team role (e.g. MEMBER). Such a token must + * still respect document visibility at request time. + */ +const seedApiTokenForUser = async ({ + userId, + teamId, + tokenName, +}: { + userId: number; + teamId: number; + tokenName: string; +}) => { + const token = `api_${alphaid(16)}`; + + await prisma.apiToken.create({ + data: { + name: tokenName, + token: hashString(token), + expires: null, + userId, + teamId, + }, + }); + + return { token }; +}; + +test.describe.configure({ + mode: 'parallel', +}); + +const downloadAuditLogPdf = (request: APIRequestContext, envelopeId: string, authToken?: string) => { + return request.get(`${API_BASE_URL}/envelope/${envelopeId}/audit-log/download`, { + headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}, + }); +}; + +const downloadCertificatePdf = (request: APIRequestContext, envelopeId: string, authToken?: string) => { + return request.get(`${API_BASE_URL}/envelope/${envelopeId}/certificate/download`, { + headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}, + }); +}; + +test.describe('Envelope certificate / audit log PDF download API V2 - access control', () => { + let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string; + + test.beforeEach(async () => { + ({ user: userA, team: teamA } = await seedUser()); + ({ token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + })); + + ({ user: userB, team: teamB } = await seedUser()); + ({ token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + })); + }); + + test('should reject audit log download without an API token', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + const res = await downloadAuditLogPdf(request, document.id); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(401); + }); + + test('should reject certificate download without an API token', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + const res = await downloadCertificatePdf(request, document.id); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(401); + }); + + test('should reject audit log download from a user in a different team', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + const res = await downloadAuditLogPdf(request, document.id, tokenB); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should reject certificate download from a user in a different team', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + const res = await downloadCertificatePdf(request, document.id, tokenB); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should reject a disabled user downloading the audit log', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + await prisma.user.update({ + where: { id: userA.id }, + data: { disabled: true }, + }); + + const res = await downloadAuditLogPdf(request, document.id, tokenA); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(401); + }); + + test('should reject a disabled user downloading the certificate', async ({ request }) => { + const document = await seedCompletedDocument(userA, teamA.id, ['recipient@test.documenso.com']); + + await prisma.user.update({ + where: { id: userA.id }, + data: { disabled: true }, + }); + + const res = await downloadCertificatePdf(request, document.id, tokenA); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(401); + }); + + test('should return 404 for a non-existent envelope id', async ({ request }) => { + const auditLogRes = await downloadAuditLogPdf(request, 'envelope_doesnotexist', tokenA); + expect(auditLogRes.status()).toBe(404); + + const certificateRes = await downloadCertificatePdf(request, 'envelope_doesnotexist', tokenA); + expect(certificateRes.status()).toBe(404); + }); +}); + +test.describe('Envelope certificate / audit log PDF download API V2 - document visibility', () => { + test.describe.configure({ + mode: 'parallel', + }); + + test('should hide an ADMIN-only document from a downgraded member (audit log)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ + userId: member.id, + teamId: team.id, + tokenName: 'member-audit-log-token', + }); + + // ADMIN-visibility document owned by the team owner - a member must not see it. + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + const res = await downloadAuditLogPdf(request, document.id, memberToken); + + // Visibility failure surfaces as not-found, matching the canonical access checks. + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should hide an ADMIN-only document from a downgraded member (certificate)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ + userId: member.id, + teamId: team.id, + tokenName: 'member-certificate-token', + }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + const res = await downloadCertificatePdf(request, document.id, memberToken); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should hide a MANAGER_AND_ABOVE document from a downgraded member (audit log)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ + userId: member.id, + teamId: team.id, + tokenName: 'member-manager-vis-token', + }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.MANAGER_AND_ABOVE }, + }); + + const res = await downloadAuditLogPdf(request, document.id, memberToken); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should hide a MANAGER_AND_ABOVE document from a downgraded member (certificate)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ + userId: member.id, + teamId: team.id, + tokenName: 'member-manager-vis-cert-token', + }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.MANAGER_AND_ABOVE }, + }); + + const res = await downloadCertificatePdf(request, document.id, memberToken); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should hide an ADMIN-only document from a downgraded manager (certificate)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const manager = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MANAGER }); + + const { token: managerToken } = await seedApiTokenForUser({ + userId: manager.id, + teamId: team.id, + tokenName: 'manager-admin-vis-cert-token', + }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + const res = await downloadCertificatePdf(request, document.id, managerToken); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow a member to download an EVERYONE-visibility document (audit log)', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ + userId: member.id, + teamId: team.id, + tokenName: 'member-everyone-vis-token', + }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.EVERYONE }, + }); + + const res = await downloadAuditLogPdf(request, document.id, memberToken); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + expect(res.headers()['content-type']).toContain('application/pdf'); + }); +}); diff --git a/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts index 3e43033ac..2c725d7f8 100644 --- a/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts +++ b/packages/trpc/server/envelope-router/download-envelope-audit-log-pdf.types.ts @@ -5,7 +5,7 @@ import type { TrpcRouteMeta } from '../trpc'; export const downloadEnvelopeAuditLogPdfMeta: TrpcRouteMeta = { openapi: { method: 'GET', - path: '/envelope/{envelopeId}/audit-log/pdf', + path: '/envelope/{envelopeId}/audit-log/download', summary: 'Download envelope audit log PDF', description: 'Download the audit log for a document as a PDF.', tags: ['Envelope'], diff --git a/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts index eeab2be69..b35e7aa11 100644 --- a/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts +++ b/packages/trpc/server/envelope-router/download-envelope-certificate-pdf.types.ts @@ -5,7 +5,7 @@ import type { TrpcRouteMeta } from '../trpc'; export const downloadEnvelopeCertificatePdfMeta: TrpcRouteMeta = { openapi: { method: 'GET', - path: '/envelope/{envelopeId}/certificate/pdf', + path: '/envelope/{envelopeId}/certificate/download', summary: 'Download envelope certificate PDF', description: 'Download the signing certificate for a completed document as a PDF.', tags: ['Envelope'], From 037170f6253d8b2bdeaf2eb0a08d04f152a41a58 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 28 Jun 2026 12:38:38 +1000 Subject: [PATCH 07/41] v2.14.0 --- apps/remix/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/remix/package.json b/apps/remix/package.json index 2f4d6fea0..22a92ee34 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -106,5 +106,5 @@ "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" }, - "version": "2.13.0" + "version": "2.14.0" } diff --git a/package-lock.json b/package-lock.json index b43306e42..cb4e523f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@documenso/root", - "version": "2.13.0", + "version": "2.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@documenso/root", - "version": "2.13.0", + "version": "2.14.0", "hasInstallScript": true, "workspaces": [ "apps/*", @@ -406,7 +406,7 @@ }, "apps/remix": { "name": "@documenso/remix", - "version": "2.13.0", + "version": "2.14.0", "dependencies": { "@cantoo/pdf-lib": "^2.5.3", "@documenso/api": "*", diff --git a/package.json b/package.json index e32496f6b..d0ffcb2b5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "apps/*", "packages/*" ], - "version": "2.13.0", + "version": "2.14.0", "scripts": { "postinstall": "patch-package", "build": "turbo run build", From 977d07330b97ce451fb834447807b9d4163fc6bd Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sun, 28 Jun 2026 15:07:33 +1000 Subject: [PATCH 08/41] fix: auto select field on drop (#3028) --- .../envelope-editor-fields-page-renderer.tsx | 89 ++++++++++++++----- .../envelope-fields.spec.ts | 1 + 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index db8f6ffbb..5cbd4addf 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx @@ -49,6 +49,13 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR const [isFieldChanging, setIsFieldChanging] = useState(false); const [pendingFieldCreation, setPendingFieldCreation] = useState(null); + /** + * Whether the field was automatically selected on creation (drag-drop or marquee). + * + * We purposefully supress the floating toolbar for newly created fields. + */ + const [isAutoSelectedField, setIsAutoSelectedField] = useState(false); + const { stage, pageLayer, konvaContainer, scaledViewport, unscaledViewport } = usePageRenderer( ({ stage, pageLayer }) => createPageCanvas(stage, pageLayer), pageData, @@ -521,13 +528,48 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR setSelectedFields(liveSelectedFieldGroups); } + // Mirror the editor's single selected field onto the canvas (Konva) selection. + // + // `addField` already marks a newly created field as the selected field, so this + // makes a field placed via the palette (drag-drop) or marquee creation show its + // resize handles immediately -- no second click needed. It also clears the canvas + // selection when the selected field is cleared (e.g. when the author starts + // placing another field), so the floating action toolbar can't intercept the next + // placement click. Runs after the render loop above so the field's group exists. + const selectedFormId = editorFields.selectedField?.formId ?? null; + const isSingleCanvasSelection = selectedKonvaFieldGroups.length === 1; + + if (selectedFormId && localPageFields.some((field) => field.formId === selectedFormId)) { + const isAlreadySelected = isSingleCanvasSelection && selectedKonvaFieldGroups[0].id() === selectedFormId; + + if (!isAlreadySelected) { + const fieldGroupToSelect = pageLayer.current.findOne(`#${selectedFormId}`); + + if (fieldGroupToSelect instanceof Konva.Group) { + setSelectedFields([fieldGroupToSelect], { isAutoSelect: true }); + } + } + } else if (selectedFormId === null && isSingleCanvasSelection) { + setSelectedFields([]); + } + // Rerender the transformer interactiveTransformer.current?.forceUpdate(); pageLayer.current.batchDraw(); - }, [localPageFields, selectedKonvaFieldGroups, overlappingFieldFormIds, isFieldChanging]); + }, [ + localPageFields, + selectedKonvaFieldGroups, + overlappingFieldFormIds, + isFieldChanging, + editorFields.selectedField?.formId, + ]); + + const setSelectedFields = (nodes: Konva.Node[], options?: { isAutoSelect?: boolean }) => { + // Any explicit (user-driven) selection shows the action toolbar; only auto-selection + // on field creation suppresses it. + setIsAutoSelectedField(Boolean(options?.isAutoSelect)); - const setSelectedFields = (nodes: Konva.Node[]) => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const fieldGroups = nodes.filter( (node) => node.hasName('field-group') && Boolean(node.getStage()) && Boolean(node.getParent()), @@ -663,25 +705,30 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR return ( <> - {selectedKonvaFieldGroups.length > 0 && interactiveTransformer.current && !isFieldChanging && ( - field.id())} - style={{ - position: 'absolute', - top: interactiveTransformer.current.y() + interactiveTransformer.current.getClientRect().height + 5 + 'px', - left: interactiveTransformer.current.x() + interactiveTransformer.current.getClientRect().width / 2 + 'px', - transform: 'translateX(-50%)', - gap: '8px', - pointerEvents: 'auto', - zIndex: 50, - }} - /> - )} + {selectedKonvaFieldGroups.length > 0 && + interactiveTransformer.current && + !isFieldChanging && + !isAutoSelectedField && ( + field.id())} + style={{ + position: 'absolute', + top: + interactiveTransformer.current.y() + interactiveTransformer.current.getClientRect().height + 5 + 'px', + left: + interactiveTransformer.current.x() + interactiveTransformer.current.getClientRect().width / 2 + 'px', + transform: 'translateX(-50%)', + gap: '8px', + pointerEvents: 'auto', + zIndex: 50, + }} + /> + )} {pendingFieldCreation && (
Date: Sun, 28 Jun 2026 15:08:11 +1000 Subject: [PATCH 09/41] feat: add field multiselect (#3031) --- .../envelope-editor-fields-page-renderer.tsx | 57 +++---- .../envelope-fields.spec.ts | 151 +++++++++++++++++- packages/app-tests/e2e/fixtures/konva.ts | 32 ++++ 3 files changed, 206 insertions(+), 34 deletions(-) diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx index 5cbd4addf..698f83651 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page-renderer.tsx @@ -244,10 +244,26 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR fieldGroup.off('transformend'); fieldGroup.off('dragend'); - // Set up field selection. - fieldGroup.on('click', () => { + // Set up field selection. Shift + click toggles this field in/out of the current + // multi-selection, so fields can be added to a group by clicking them -- + // complementing marquee drag-selection. A plain click (no modifier) selects just + // this field. + fieldGroup.on('click', (event) => { removePendingField(); - setSelectedFields([fieldGroup]); + + const isMultiSelectModifier = event.evt.shiftKey; + + if (isMultiSelectModifier) { + const currentNodes = interactiveTransformer.current?.nodes() ?? []; + const isAlreadySelected = currentNodes.includes(fieldGroup); + + setSelectedFields( + isAlreadySelected ? currentNodes.filter((node) => node !== fieldGroup) : [...currentNodes, fieldGroup], + ); + } else { + setSelectedFields([fieldGroup]); + } + pageLayer.current?.batchDraw(); }); @@ -452,43 +468,18 @@ export const EnvelopeEditorFieldsPageRenderer = ({ pageData }: { pageData: PageR } }); - // Clicks should select/deselect shapes + // Clicking empty stage area clears the selection. Field clicks -- including + // Shift+click multi-select -- are handled by each field group's own click + // handler in `unsafeRenderFieldOnLayer`. currentStage.on('click tap', (e) => { - // if we are selecting with rect, do nothing + // If we are selecting with the marquee rectangle, do nothing. if (selectionRectangle.visible() && selectionRectangle.width() > 0 && selectionRectangle.height() > 0) { return; } - // If empty area clicked, remove all selections + // If empty area clicked, remove all selections. if (e.target === stage.current) { setSelectedFields([]); - return; - } - - // Do nothing if field not clicked, or if field is not editable - if (!e.target.hasName('field-group') || e.target.draggable() === false) { - return; - } - - // do we pressed shift or ctrl? - const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey; - const isSelected = transformer.nodes().indexOf(e.target) >= 0; - - if (!metaPressed && !isSelected) { - // if no key pressed and the node is not selected - // select just one - setSelectedFields([e.target]); - } else if (metaPressed && isSelected) { - // if we pressed keys and node was selected - // we need to remove it from selection: - const nodes = transformer.nodes().slice(); // use slice to have new copy of array - // remove node from array - nodes.splice(nodes.indexOf(e.target), 1); - setSelectedFields(nodes); - } else if (metaPressed && !isSelected) { - // add the node into selection - const nodes = transformer.nodes().concat([e.target]); - setSelectedFields(nodes); } }); diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-fields.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-fields.spec.ts index f9c3515d4..0a4c3ab73 100644 --- a/packages/app-tests/e2e/envelope-editor-v2/envelope-fields.spec.ts +++ b/packages/app-tests/e2e/envelope-editor-v2/envelope-fields.spec.ts @@ -20,7 +20,7 @@ import { type TEnvelopeEditorSurface, } from '../fixtures/envelope-editor'; import { expectToastTextToBeVisible } from '../fixtures/generic'; -import { getKonvaElementCountForPage } from '../fixtures/konva'; +import { getKonvaElementCountForPage, getKonvaTransformerNodeCountForPage } from '../fixtures/konva'; type TFieldFlowResult = { externalId: string; @@ -99,6 +99,17 @@ const selectFieldOnCanvas = async (root: Page, position: { x: number; y: number await canvas.click({ position, force: true }); }; +/** + * Shift+click a field on the canvas to toggle it in/out of the current multi-selection. + */ +const shiftClickFieldOnCanvas = async (root: Page, position: { x: number; y: number }) => { + const canvas = root.locator('.konva-container canvas').first(); + await expect(canvas).toBeVisible(); + await root.waitForTimeout(300); + // Use force:true to bypass any floating action toolbar buttons that may intercept clicks. + await canvas.click({ position, modifiers: ['Shift'], force: true }); +}; + const runAddAndPersistSignatureTextFields = async (surface: TEnvelopeEditorSurface): Promise => { const externalId = `e2e-fields-${nanoid()}`; @@ -761,9 +772,106 @@ const assertChangeFieldTypePersistedInDatabase = async ({ expect(actualMetaTypes).toEqual(['date', 'date']); }; +// --- Shift+click multi-select flow --- + +type TShiftClickFlowResult = { + externalId: string; +}; + +const SHIFT_CLICK_FIELD_POSITIONS = { + signature: { x: 150, y: 120 }, + text: { x: 150, y: 260 }, + name: { x: 150, y: 400 }, +}; + +const runShiftClickMultiSelectFlow = async (surface: TEnvelopeEditorSurface): Promise => { + const externalId = `e2e-shift-click-${nanoid()}`; + const root = surface.root; + + if (surface.isEmbedded && !surface.envelopeId) { + await addEnvelopeItemPdf(root, 'embedded-fields.pdf'); + } + + await updateExternalId(surface, externalId); + await setupRecipientsForFieldPlacement(surface); + + await clickEnvelopeEditorStep(root, 'addFields'); + await expect(root.locator('.konva-container canvas').first()).toBeVisible(); + + // Place three fields, spaced far enough apart that their action toolbars don't + // overlap a neighbouring field's click target. + await placeFieldOnPdf(root, 'Signature', SHIFT_CLICK_FIELD_POSITIONS.signature); + await placeFieldOnPdf(root, 'Text', SHIFT_CLICK_FIELD_POSITIONS.text); + await placeFieldOnPdf(root, 'Name', SHIFT_CLICK_FIELD_POSITIONS.name); + expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(3); + + // A plain click selects exactly one field. + await selectFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature); + await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1); + + // Shift+click a second field ADDS it to the selection (the new behaviour). + await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.text); + await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2); + + // Shift+click an already-selected field REMOVES it from the selection. + await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature); + await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(1); + + // Shift+click it again RE-ADDS it, leaving Signature + Text selected and Name excluded. + await shiftClickFieldOnCanvas(root, SHIFT_CLICK_FIELD_POSITIONS.signature); + await expect.poll(() => getKonvaTransformerNodeCountForPage(root, 1)).toBe(2); + + // Delete the two selected fields via the floating action toolbar. Only the + // un-selected Name field should remain -- proving the multi-selection contained + // exactly the two Shift-clicked fields. + await expect(root.locator('button[title="Remove"]')).toBeVisible(); + await root.locator('button[title="Remove"]').click(); + expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1); + + // Navigate away and back to verify persistence. + await clickEnvelopeEditorStep(root, 'upload'); + await clickEnvelopeEditorStep(root, 'addFields'); + expect(await getKonvaElementCountForPage(root, 1, '.field-group')).toBe(1); + + return { externalId }; +}; + +const assertShiftClickMultiSelectPersistedInDatabase = async ({ + surface, + externalId, +}: { + surface: TEnvelopeEditorSurface; + externalId: string; +}) => { + const envelope = await prisma.envelope.findFirstOrThrow({ + where: { + externalId, + userId: surface.userId, + teamId: surface.teamId, + type: surface.envelopeType, + }, + orderBy: { createdAt: 'desc' }, + include: { fields: true }, + }); + + // Signature + Text were multi-selected via Shift+click and deleted; only Name remains. + expect(envelope.fields).toHaveLength(1); + expect(envelope.fields[0].type).toBe(FieldType.NAME); +}; + // --- Test describe blocks --- test.describe('document editor', () => { + test('shift+click adds and removes fields from the selection', async ({ page }) => { + const surface = await openDocumentEnvelopeEditor(page); + const result = await runShiftClickMultiSelectFlow(surface); + + await assertShiftClickMultiSelectPersistedInDatabase({ + surface, + ...result, + }); + }); + test('add and persist signature/text fields', async ({ page }) => { const surface = await openDocumentEnvelopeEditor(page); const result = await runAddAndPersistSignatureTextFields(surface); @@ -816,6 +924,16 @@ test.describe('document editor', () => { }); test.describe('template editor', () => { + test('shift+click adds and removes fields from the selection', async ({ page }) => { + const surface = await openTemplateEnvelopeEditor(page); + const result = await runShiftClickMultiSelectFlow(surface); + + await assertShiftClickMultiSelectPersistedInDatabase({ + surface, + ...result, + }); + }); + test('add and persist signature/text fields', async ({ page }) => { const surface = await openTemplateEnvelopeEditor(page); const result = await runAddAndPersistSignatureTextFields(surface); @@ -868,6 +986,21 @@ test.describe('template editor', () => { }); test.describe('embedded create', () => { + test('shift+click adds and removes fields from the selection', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'DOCUMENT', + tokenNamePrefix: 'e2e-embed-shift-click', + }); + const result = await runShiftClickMultiSelectFlow(surface); + + await persistEmbeddedEnvelope(surface); + + await assertShiftClickMultiSelectPersistedInDatabase({ + surface, + ...result, + }); + }); + test('add and persist signature/text fields', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'DOCUMENT', @@ -945,6 +1078,22 @@ test.describe('embedded create', () => { }); test.describe('embedded edit', () => { + test('shift+click adds and removes fields from the selection', async ({ page }) => { + const surface = await openEmbeddedEnvelopeEditor(page, { + envelopeType: 'TEMPLATE', + mode: 'edit', + tokenNamePrefix: 'e2e-embed-shift-click', + }); + const result = await runShiftClickMultiSelectFlow(surface); + + await persistEmbeddedEnvelope(surface); + + await assertShiftClickMultiSelectPersistedInDatabase({ + surface, + ...result, + }); + }); + test('add and persist signature/text fields', async ({ page }) => { const surface = await openEmbeddedEnvelopeEditor(page, { envelopeType: 'TEMPLATE', diff --git a/packages/app-tests/e2e/fixtures/konva.ts b/packages/app-tests/e2e/fixtures/konva.ts index 316eb46f4..87973ab97 100644 --- a/packages/app-tests/e2e/fixtures/konva.ts +++ b/packages/app-tests/e2e/fixtures/konva.ts @@ -16,3 +16,35 @@ export const getKonvaElementCountForPage = async (page: Page, pageNumber: number { pageNumber, elementSelector }, ); }; + +/** + * Returns how many field groups are currently attached to the page's Konva + * transformer, i.e. the size of the active canvas selection. Used to assert + * multi-select behaviour (marquee drag and Shift+click). + */ +export const getKonvaTransformerNodeCountForPage = async (page: Page, pageNumber: number) => { + await page.locator('.konva-container canvas').first().waitFor({ state: 'visible' }); + + return await page.evaluate( + ({ pageNumber }) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const konva: typeof Konva = (window as unknown as { Konva: typeof Konva }).Konva; + + const stage = konva.stages.find((stage) => stage.attrs.id === `page-${pageNumber}`); + + if (!stage) { + return 0; + } + + const transformer = stage.find('Transformer')[0]; + + if (!transformer) { + return 0; + } + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return (transformer as Konva.Transformer).nodes().length; + }, + { pageNumber }, + ); +}; From 381293af0c6b198eef0b2ae3cfca87b754c287b1 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 28 Jun 2026 22:01:20 +1000 Subject: [PATCH 10/41] fix: invite email placeholder (#3045) - **fix: interpolate inviterEmail in invite email mailto link** - **fix: add alt attributes to email template images** --- .../template-document-completed.tsx | 8 ++++++-- .../template-document-pending.tsx | 2 +- .../template-document-recipient-signed.tsx | 6 +++++- .../template-document-self-signed.tsx | 14 +++++++++++--- .../email/template-components/template-image.tsx | 2 +- packages/email/templates/document-invite.tsx | 2 +- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/email/template-components/template-document-completed.tsx b/packages/email/template-components/template-document-completed.tsx index 7742c90fa..ec453f315 100644 --- a/packages/email/template-components/template-document-completed.tsx +++ b/packages/email/template-components/template-document-completed.tsx @@ -28,7 +28,11 @@ export const TemplateDocumentCompleted = ({
- + Completed @@ -47,7 +51,7 @@ export const TemplateDocumentCompleted = ({ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href={downloadLink} > - + Download
diff --git a/packages/email/template-components/template-document-pending.tsx b/packages/email/template-components/template-document-pending.tsx index 5022b8b87..c44ae5c81 100644 --- a/packages/email/template-components/template-document-pending.tsx +++ b/packages/email/template-components/template-document-pending.tsx @@ -21,7 +21,7 @@ export const TemplateDocumentPending = ({ documentName, assetBaseUrl }: Template
- + Waiting for others diff --git a/packages/email/template-components/template-document-recipient-signed.tsx b/packages/email/template-components/template-document-recipient-signed.tsx index c35a0f316..42ebe0e10 100644 --- a/packages/email/template-components/template-document-recipient-signed.tsx +++ b/packages/email/template-components/template-document-recipient-signed.tsx @@ -30,7 +30,11 @@ export const TemplateDocumentRecipientSigned = ({
- + Completed diff --git a/packages/email/template-components/template-document-self-signed.tsx b/packages/email/template-components/template-document-self-signed.tsx index 4759ea06f..852843577 100644 --- a/packages/email/template-components/template-document-self-signed.tsx +++ b/packages/email/template-components/template-document-self-signed.tsx @@ -26,7 +26,11 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ
- + Completed @@ -51,7 +55,11 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ href={signUpUrl} className="mr-4 rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" > - + Create account @@ -59,7 +67,7 @@ export const TemplateDocumentSelfSigned = ({ documentName, assetBaseUrl }: Templ className="rounded-lg border border-border border-solid px-4 py-2 text-center font-medium text-foreground text-sm no-underline" href="https://documenso.com/pricing" > - + View plans
diff --git a/packages/email/template-components/template-image.tsx b/packages/email/template-components/template-image.tsx index 8f821c10f..b16bcfe60 100644 --- a/packages/email/template-components/template-image.tsx +++ b/packages/email/template-components/template-image.tsx @@ -11,7 +11,7 @@ export const TemplateImage = ({ assetBaseUrl, className, staticAsset }: Template return new URL(path, assetBaseUrl).toString(); }; - return ; + return ; }; export default TemplateImage; diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 5e8d3a6d4..277f28859 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -85,7 +85,7 @@ export const DocumentInviteEmailTemplate = ({ {inviterName}{' '} - + ({inviterEmail}) From 8f683932419341683b57be1c056dbda12df8b598 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Mon, 29 Jun 2026 13:15:13 +1000 Subject: [PATCH 11/41] fix: tighten permission and validation checks (#3046) --- .../document-signing-attachments-popover.tsx | 3 +- .../document/document-attachments-popover.tsx | 5 +- .../embedded-editor-attachment-popover.tsx | 5 +- .../document-visibility-access.spec.ts | 89 +++++++++++++ .../attachment-url-validation.spec.ts | 70 ++++++++++ .../attachment-visibility-access.spec.ts | 121 ++++++++++++++++++ .../organisation-permission-hierarchy.spec.ts | 64 +++++++++ .../recipient-visibility-access.spec.ts | 72 +++++++++++ .../e2e/teams/team-profile-access.spec.ts | 53 ++++++++ .../server-only/document/cancel-document.ts | 28 ++-- .../server-only/document/delete-document.ts | 35 +++-- .../envelope-attachment/create-attachment.ts | 14 +- .../envelope-attachment/delete-attachment.ts | 23 +++- .../find-attachments-by-envelope-id.ts | 14 +- .../envelope-attachment/update-attachment.ts | 23 +++- .../public-api/get-user-by-token.ts | 44 ------- .../recipient/get-recipient-by-id.ts | 22 ++++ .../share/create-or-get-share-link.ts | 27 ++++ .../team/get-team-public-profile.ts | 24 ++-- .../team/update-team-public-profile.ts | 7 +- .../lib/server-only/user/forgot-password.ts | 41 +++--- .../server-only/webhooks/zapier/subscribe.ts | 40 +++++- .../webhooks/zapier/unsubscribe.ts | 30 ++++- packages/lib/utils/is-http-url.test.ts | 45 +++++++ packages/lib/utils/is-http-url.ts | 32 +++++ .../attachment/create-attachment.types.ts | 3 +- .../attachment/update-attachment.types.ts | 3 +- .../get-envelope-recipient.ts | 22 ++++ .../update-organisation-group.ts | 18 +++ 29 files changed, 835 insertions(+), 142 deletions(-) create mode 100644 packages/app-tests/e2e/documents/document-visibility-access.spec.ts create mode 100644 packages/app-tests/e2e/envelope-editor-v2/attachment-url-validation.spec.ts create mode 100644 packages/app-tests/e2e/envelope-editor-v2/attachment-visibility-access.spec.ts create mode 100644 packages/app-tests/e2e/recipient/recipient-visibility-access.spec.ts create mode 100644 packages/app-tests/e2e/teams/team-profile-access.spec.ts delete mode 100644 packages/lib/server-only/public-api/get-user-by-token.ts create mode 100644 packages/lib/utils/is-http-url.test.ts create mode 100644 packages/lib/utils/is-http-url.ts diff --git a/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx b/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx index e472bb5c9..0ab921a59 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx @@ -1,3 +1,4 @@ +import { toSafeHref } from '@documenso/lib/utils/is-http-url'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; @@ -53,7 +54,7 @@ export const DocumentSigningAttachmentsPopover = ({ {attachments?.data.map((attachment) => ( ; @@ -156,7 +157,7 @@ export const DocumentAttachmentsPopover = ({

{attachment.label}

; @@ -117,7 +118,7 @@ export const EmbeddedEditorAttachmentPopover = ({

{attachment.label}

{ + try { + await getEnvelopeWhereInput({ + id: { type: 'envelopeId', id: envelopeId }, + userId, + teamId, + type: null, + }).then(({ envelopeWhereInput }) => prisma.envelope.findFirstOrThrow({ where: envelopeWhereInput })); + + return true; + } catch { + return false; + } +}; + +test('[DOCUMENTS]: a member cannot delete a document with restricted visibility', async () => { + const { user: owner, team } = await seedUser(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const envelope = await seedBlankDocument(owner, team.id, { + createDocumentOptions: { + visibility: DocumentVisibility.ADMIN, + status: DocumentStatus.DRAFT, + }, + }); + + // The member cannot read an ADMIN-visibility document, so they must not be + // able to delete it either. + expect(await canReadEnvelope(envelope.id, member.id, team.id)).toBe(false); + + await expect( + deleteDocument({ + id: { type: 'envelopeId', id: envelope.id }, + userId: member.id, + teamId: team.id, + requestMetadata, + }), + ).rejects.toThrow(); + + const stillExists = await prisma.envelope.findUnique({ where: { id: envelope.id } }); + expect(stillExists).not.toBeNull(); +}); + +test('[DOCUMENTS]: a manager cannot cancel a document with restricted visibility', async () => { + const { user: owner, team } = await seedUser(); + const manager = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MANAGER }); + + const envelope = await seedBlankDocument(owner, team.id, { + createDocumentOptions: { + visibility: DocumentVisibility.ADMIN, + status: DocumentStatus.PENDING, + }, + }); + + // A manager outranks a member but still cannot read an ADMIN-visibility + // document, so cancellation must be blocked despite the sufficient role. + expect(await canReadEnvelope(envelope.id, manager.id, team.id)).toBe(false); + + await expect( + cancelDocument({ + id: { type: 'envelopeId', id: envelope.id }, + userId: manager.id, + teamId: team.id, + reason: 'test-cancel', + requestMetadata, + }), + ).rejects.toThrow(); + + const after = await prisma.envelope.findUnique({ where: { id: envelope.id } }); + expect(after?.status).toBe(DocumentStatus.PENDING); +}); diff --git a/packages/app-tests/e2e/envelope-editor-v2/attachment-url-validation.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/attachment-url-validation.spec.ts new file mode 100644 index 000000000..c3228a057 --- /dev/null +++ b/packages/app-tests/e2e/envelope-editor-v2/attachment-url-validation.spec.ts @@ -0,0 +1,70 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { hashString } from '@documenso/lib/server-only/auth/hash'; +import { alphaid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { seedBlankDocument } from '@documenso/prisma/seed/documents'; +import { seedTeam } from '@documenso/prisma/seed/teams'; +import { expect, test } from '@playwright/test'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); +const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`; + +test.describe.configure({ mode: 'parallel' }); + +const seedApiTokenForUser = async ({ userId, teamId }: { userId: number; teamId: number }) => { + const token = `api_${alphaid(16)}`; + + await prisma.apiToken.create({ + data: { name: 'attachment-url-test', token: hashString(token), expires: null, userId, teamId }, + }); + + return { token }; +}; + +/** + * Attachment URLs are rendered as link hrefs, so they must be restricted to + * http(s). The API must reject any other scheme. + */ +const NON_HTTP_URLS = [ + 'javascript:alert(document.cookie)', + 'data:text/html,', + 'vbscript:msgbox(1)', + 'file:///etc/passwd', +]; + +test('[ATTACHMENTS]: rejects attachment URLs with a non-http(s) protocol', async ({ request }) => { + const { team, owner } = await seedTeam(); + const { token } = await seedApiTokenForUser({ userId: owner.id, teamId: team.id }); + + const envelope = await seedBlankDocument(owner, team.id); + + for (const url of NON_HTTP_URLS) { + const res = await request.post(`${API_BASE_URL}/envelope/attachment/create`, { + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + data: { envelopeId: envelope.id, data: { label: 'attachment', data: url } }, + }); + + expect(res.ok(), `expected ${url} to be rejected`).toBe(false); + } + + const attachments = await prisma.envelopeAttachment.findMany({ where: { envelopeId: envelope.id } }); + expect(attachments).toHaveLength(0); +}); + +test('[ATTACHMENTS]: accepts attachment URLs with an http(s) protocol', async ({ request }) => { + const { team, owner } = await seedTeam(); + const { token } = await seedApiTokenForUser({ userId: owner.id, teamId: team.id }); + + const envelope = await seedBlankDocument(owner, team.id); + + const res = await request.post(`${API_BASE_URL}/envelope/attachment/create`, { + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + data: { envelopeId: envelope.id, data: { label: 'safe', data: 'https://example.com/file.pdf' } }, + }); + + expect(res.ok()).toBe(true); + + const attachments = await prisma.envelopeAttachment.findMany({ where: { envelopeId: envelope.id } }); + expect(attachments).toHaveLength(1); + expect(attachments[0].data).toBe('https://example.com/file.pdf'); +}); diff --git a/packages/app-tests/e2e/envelope-editor-v2/attachment-visibility-access.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/attachment-visibility-access.spec.ts new file mode 100644 index 000000000..f47a76c1f --- /dev/null +++ b/packages/app-tests/e2e/envelope-editor-v2/attachment-visibility-access.spec.ts @@ -0,0 +1,121 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { hashString } from '@documenso/lib/server-only/auth/hash'; +import { alphaid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client'; +import { seedBlankDocument } from '@documenso/prisma/seed/documents'; +import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; +import { type APIRequestContext, expect, test } from '@playwright/test'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); +const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`; + +test.describe.configure({ mode: 'parallel' }); + +const seedApiTokenForUser = async ({ userId, teamId }: { userId: number; teamId: number }) => { + const token = `api_${alphaid(16)}`; + + await prisma.apiToken.create({ + data: { name: 'attachment-access-test', token: hashString(token), expires: null, userId, teamId }, + }); + + return { token }; +}; + +const canReadEnvelope = async (request: APIRequestContext, token: string, envelopeId: string) => { + const res = await request.get(`${API_BASE_URL}/envelope/${envelopeId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + return res.ok(); +}; + +/** + * Attachment create/update/delete/list must enforce document visibility, not + * just team membership. A member whose visibility tier excludes a restricted + * envelope must not be able to read or mutate its attachments. + */ +test('[ATTACHMENTS]: a member cannot create or delete attachments on a restricted document', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ userId: member.id, teamId: team.id }); + + const envelope = await seedBlankDocument(owner, team.id, { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + expect(await canReadEnvelope(request, memberToken, envelope.id)).toBe(false); + + const createRes = await request.post(`${API_BASE_URL}/envelope/attachment/create`, { + headers: { Authorization: `Bearer ${memberToken}`, 'Content-Type': 'application/json' }, + data: { envelopeId: envelope.id, data: { label: 'attachment', data: 'https://example.com' } }, + }); + + expect(createRes.ok()).toBe(false); + + // No attachment should have been created. + const attachments = await prisma.envelopeAttachment.findMany({ where: { envelopeId: envelope.id } }); + expect(attachments).toHaveLength(0); +}); + +test('[ATTACHMENTS]: a member cannot update an attachment on a restricted document', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: ownerToken } = await seedApiTokenForUser({ userId: owner.id, teamId: team.id }); + const { token: memberToken } = await seedApiTokenForUser({ userId: member.id, teamId: team.id }); + + const envelope = await seedBlankDocument(owner, team.id, { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + // The owner (who can see the document) creates the attachment. + const createRes = await request.post(`${API_BASE_URL}/envelope/attachment/create`, { + headers: { Authorization: `Bearer ${ownerToken}`, 'Content-Type': 'application/json' }, + data: { envelopeId: envelope.id, data: { label: 'original', data: 'https://example.com/original' } }, + }); + expect(createRes.ok()).toBe(true); + const attachment = await createRes.json(); + + expect(await canReadEnvelope(request, memberToken, envelope.id)).toBe(false); + + const updateRes = await request.post(`${API_BASE_URL}/envelope/attachment/update`, { + headers: { Authorization: `Bearer ${memberToken}`, 'Content-Type': 'application/json' }, + data: { id: attachment.id, data: { label: 'tampered', data: 'https://example.com/tampered' } }, + }); + + expect(updateRes.ok()).toBe(false); + + const persisted = await prisma.envelopeAttachment.findUnique({ where: { id: attachment.id } }); + expect(persisted?.label).toBe('original'); +}); + +test('[ATTACHMENTS]: a member cannot list attachments on a restricted document', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: ownerToken } = await seedApiTokenForUser({ userId: owner.id, teamId: team.id }); + const { token: memberToken } = await seedApiTokenForUser({ userId: member.id, teamId: team.id }); + + const envelope = await seedBlankDocument(owner, team.id, { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + await request.post(`${API_BASE_URL}/envelope/attachment/create`, { + headers: { Authorization: `Bearer ${ownerToken}`, 'Content-Type': 'application/json' }, + data: { envelopeId: envelope.id, data: { label: 'restricted', data: 'https://example.com/restricted' } }, + }); + + expect(await canReadEnvelope(request, memberToken, envelope.id)).toBe(false); + + const findRes = await request.get(`${API_BASE_URL}/envelope/attachment?envelopeId=${envelope.id}`, { + headers: { Authorization: `Bearer ${memberToken}` }, + }); + + expect(findRes.ok()).toBe(false); + + const body = findRes.ok() ? await findRes.json() : null; + const attachments = body?.data ?? []; + expect(attachments).toHaveLength(0); +}); diff --git a/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts b/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts index b688c7cd9..23483a48e 100644 --- a/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts +++ b/packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts @@ -340,3 +340,67 @@ test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: leaving an organisation', () expect(deleted).toBeNull(); }); }); + +test.describe('[ORGANISATION_PERMISSION_HIERARCHY]: group membership scoping', () => { + test('cannot add a member from another organisation to a group', async ({ page }) => { + // Organisation A, where the actor is the owner/admin. + const { user: actor, organisation: organisationA } = await seedUser({ + isPersonalOrganisation: false, + }); + + // A separate organisation B with a member the actor has no authority over. + const { organisation: organisationB } = await seedUser({ isPersonalOrganisation: false }); + const [foreignUser] = await seedOrganisationMembers({ + members: [{ name: 'Foreign', organisationRole: 'MEMBER' }], + organisationId: organisationB.id, + }); + + const foreignMember = await getOrganisationMember(foreignUser.id, organisationB.id); + + // A custom group the actor legitimately controls in organisation A. + const groupA = await createCustomGroup(organisationA.id, 'MEMBER'); + + await apiSignin({ page, email: actor.email }); + + const res = await trpcMutation(page, 'organisation.group.update', { + id: groupA.id, + memberIds: [foreignMember.id], + }); + + expect(res.ok()).toBeFalsy(); + + const injectedMembership = await prisma.organisationGroupMember.findFirst({ + where: { groupId: groupA.id, organisationMemberId: foreignMember.id }, + }); + + expect(injectedMembership).toBeNull(); + }); + + test('can add a member from the same organisation to a group (positive control)', async ({ page }) => { + const { user: actor, organisation } = await seedUser({ isPersonalOrganisation: false }); + + const [memberUser] = await seedOrganisationMembers({ + members: [{ name: 'Member', organisationRole: 'MEMBER' }], + organisationId: organisation.id, + }); + + const member = await getOrganisationMember(memberUser.id, organisation.id); + + const group = await createCustomGroup(organisation.id, 'MEMBER'); + + await apiSignin({ page, email: actor.email }); + + const res = await trpcMutation(page, 'organisation.group.update', { + id: group.id, + memberIds: [member.id], + }); + + expect(res.ok()).toBeTruthy(); + + const membership = await prisma.organisationGroupMember.findFirst({ + where: { groupId: group.id, organisationMemberId: member.id }, + }); + + expect(membership).not.toBeNull(); + }); +}); diff --git a/packages/app-tests/e2e/recipient/recipient-visibility-access.spec.ts b/packages/app-tests/e2e/recipient/recipient-visibility-access.spec.ts new file mode 100644 index 000000000..50de44307 --- /dev/null +++ b/packages/app-tests/e2e/recipient/recipient-visibility-access.spec.ts @@ -0,0 +1,72 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { hashString } from '@documenso/lib/server-only/auth/hash'; +import { alphaid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client'; +import { seedCompletedDocument } from '@documenso/prisma/seed/documents'; +import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; +import { expect, test } from '@playwright/test'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); +const API_BASE_URL = `${WEBAPP_BASE_URL}/api/v2-beta`; + +test.describe.configure({ mode: 'parallel' }); + +const seedApiTokenForUser = async ({ userId, teamId }: { userId: number; teamId: number }) => { + const token = `api_${alphaid(16)}`; + + await prisma.apiToken.create({ + data: { name: 'recipient-access-test', token: hashString(token), expires: null, userId, teamId }, + }); + + return { token }; +}; + +/** + * Reading a recipient exposes its signing token (a bearer credential), so the + * recipient read must enforce document visibility — a member who cannot read a + * restricted document must not be able to read its recipients either. This + * mirrors the field read, which is asserted as a control below. + */ +test('[RECIPIENT]: a member cannot read a recipient of a restricted document', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ userId: member.id, teamId: team.id }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + const recipient = await prisma.recipient.findFirstOrThrow({ where: { envelopeId: document.id } }); + + const res = await request.get(`${API_BASE_URL}/envelope/recipient/${recipient.id}`, { + headers: { Authorization: `Bearer ${memberToken}` }, + }); + + expect(res.status()).toBe(404); + + const body = res.ok() ? await res.json() : null; + expect(body?.token).toBeUndefined(); +}); + +test('[RECIPIENT]: a member cannot read a field of a restricted document', async ({ request }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + const { token: memberToken } = await seedApiTokenForUser({ userId: member.id, teamId: team.id }); + + const document = await seedCompletedDocument(owner, team.id, ['recipient@test.documenso.com'], { + createDocumentOptions: { visibility: DocumentVisibility.ADMIN }, + }); + + const field = await prisma.field.findFirst({ where: { envelopeId: document.id } }); + + test.skip(!field, 'No field seeded on completed document'); + + const res = await request.get(`${API_BASE_URL}/envelope/field/${field!.id}`, { + headers: { Authorization: `Bearer ${memberToken}` }, + }); + + expect(res.status()).toBe(404); +}); diff --git a/packages/app-tests/e2e/teams/team-profile-access.spec.ts b/packages/app-tests/e2e/teams/team-profile-access.spec.ts new file mode 100644 index 000000000..ecff18fed --- /dev/null +++ b/packages/app-tests/e2e/teams/team-profile-access.spec.ts @@ -0,0 +1,53 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { prisma } from '@documenso/prisma'; +import { TeamMemberRole } from '@documenso/prisma/client'; +import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; +import { expect, test } from '@playwright/test'; + +import { apiSignin } from '../fixtures/authentication'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); + +test.describe.configure({ mode: 'parallel' }); + +/** + * Editing the team public profile is a team-management action and must require + * MANAGE_TEAM, consistent with renaming the team or changing its URL. + */ +test('[TEAMS]: a member cannot edit the team public profile', async ({ page }) => { + const { team, owner } = await seedTeam(); + const member = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + await apiSignin({ page, email: member.email }); + + const profileRes = await page.context().request.post(`${WEBAPP_BASE_URL}/api/trpc/team.update`, { + headers: { 'content-type': 'application/json', 'x-team-id': team.id.toString() }, + data: JSON.stringify({ + json: { + teamId: team.id, + data: { profileEnabled: true, profileBio: 'edited-by-member' }, + }, + }), + }); + + expect(profileRes.status()).not.toBe(200); + + const profile = await prisma.teamProfile.findUnique({ where: { teamId: team.id } }); + expect(profile?.enabled ?? false).toBe(false); + expect(profile?.bio ?? '').not.toBe('edited-by-member'); + + // The name/url path of the same route is also management-gated. + const nameRes = await page.context().request.post(`${WEBAPP_BASE_URL}/api/trpc/team.update`, { + headers: { 'content-type': 'application/json', 'x-team-id': team.id.toString() }, + data: JSON.stringify({ + json: { teamId: team.id, data: { name: 'renamed-by-member' } }, + }), + }); + + expect(nameRes.status()).not.toBe(200); + + const reloaded = await prisma.team.findUnique({ where: { id: team.id } }); + expect(reloaded?.name).not.toBe('renamed-by-member'); + + expect(owner.id).toBeTruthy(); +}); diff --git a/packages/lib/server-only/document/cancel-document.ts b/packages/lib/server-only/document/cancel-document.ts index 3ac41ca6e..6496e0908 100644 --- a/packages/lib/server-only/document/cancel-document.ts +++ b/packages/lib/server-only/document/cancel-document.ts @@ -8,8 +8,9 @@ import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../ import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import type { EnvelopeIdOptions } from '../../utils/envelope'; -import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; +import { mapSecondaryIdToDocumentId } from '../../utils/envelope'; import { isMemberManagerOrAbove } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getMemberRoles } from '../team/get-member-roles'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -22,9 +23,18 @@ export type CancelDocumentOptions = { }; export const cancelDocument = async ({ id, userId, teamId, reason, requestMetadata }: CancelDocumentOptions) => { - // Note: This is an unsafe request, we validate the ownership/permission later in the function. - const envelope = await prisma.envelope.findUnique({ - where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), + // Resolve the envelope through the visibility-aware helper so the caller must + // have read access (ownership OR team membership with sufficient visibility OR + // team-email). This prevents cancelling a document the caller cannot see. + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + userId, + teamId, + type: EnvelopeType.DOCUMENT, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, documentMeta: true, @@ -49,16 +59,6 @@ export const cancelDocument = async ({ id, userId, teamId, reason, requestMetada .then((roles) => roles.teamRole) .catch(() => null); - const isUserTeamMember = teamRole !== null; - - // Callers with no relationship to the document must not be able to determine - // whether it exists, so respond as if it was not found. - if (!isUserOwner && !isUserTeamMember) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', - }); - } - const isPrivilegedTeamMember = teamRole && isMemberManagerOrAbove(teamRole); // The document is visible to the caller, but cancelling requires elevated permissions. diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index 8eed2702f..0a4456d5c 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -13,7 +13,7 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { isRecipientEmailValidForSending } from '../../utils/recipients'; import { getEmailContext } from '../email/get-email-context'; -import { getMemberRoles } from '../team/get-member-roles'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type DeleteDocumentOptions = { @@ -36,7 +36,9 @@ export const deleteDocument = async ({ id, userId, teamId, requestMetadata }: De }); } - // Note: This is an unsafe request, we validate the ownership later in the function. + // Note: This is an unsafe request. It is used purely to resolve the recipient + // self-hide path below. The authoritative delete authorization is performed + // via the visibility-aware `getEnvelopeWhereInput` helper. const envelope = await prisma.envelope.findUnique({ where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), include: { @@ -51,27 +53,36 @@ export const deleteDocument = async ({ id, userId, teamId, requestMetadata }: De }); } - const isUserTeamMember = await getMemberRoles({ - teamId: envelope.teamId, - reference: { - type: 'User', - id: userId, - }, + // Determine whether the user has authorized delete access using the + // visibility-aware helper. This enforces ownership OR (team membership AND + // the document's visibility is permitted for the member's role) OR team-email + // access. A bare team member without sufficient visibility will resolve to + // null here and therefore must not be able to delete the document. + const hasDeleteAccess = await getEnvelopeWhereInput({ + id, + userId, + teamId, + type: EnvelopeType.DOCUMENT, }) - .then(() => true) + .then(({ envelopeWhereInput }) => + prisma.envelope.findFirst({ + where: envelopeWhereInput, + select: { id: true }, + }), + ) + .then((result) => Boolean(result)) .catch(() => false); - const isUserOwner = envelope.userId === userId; const userRecipient = envelope.recipients.find((recipient) => recipient.email === user.email); - if (!isUserOwner && !isUserTeamMember && !userRecipient) { + if (!hasDeleteAccess && !userRecipient) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Not allowed', }); } // Handle hard or soft deleting the actual document if user has permission. - if (isUserOwner || isUserTeamMember) { + if (hasDeleteAccess) { await handleDocumentOwnerDelete({ envelope, user, diff --git a/packages/lib/server-only/envelope-attachment/create-attachment.ts b/packages/lib/server-only/envelope-attachment/create-attachment.ts index b753378cf..0e16639f0 100644 --- a/packages/lib/server-only/envelope-attachment/create-attachment.ts +++ b/packages/lib/server-only/envelope-attachment/create-attachment.ts @@ -2,7 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@prisma/client'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type CreateAttachmentOptions = { envelopeId: string; @@ -15,11 +15,15 @@ export type CreateAttachmentOptions = { }; export const createAttachment = async ({ envelopeId, teamId, userId, data }: CreateAttachmentOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { type: 'envelopeId', id: envelopeId }, + userId, + teamId, + type: null, + }); + const envelope = await prisma.envelope.findFirst({ - where: { - id: envelopeId, - team: buildTeamWhereQuery({ teamId, userId }), - }, + where: envelopeWhereInput, }); if (!envelope) { diff --git a/packages/lib/server-only/envelope-attachment/delete-attachment.ts b/packages/lib/server-only/envelope-attachment/delete-attachment.ts index 67f4e7974..f04e6cbb9 100644 --- a/packages/lib/server-only/envelope-attachment/delete-attachment.ts +++ b/packages/lib/server-only/envelope-attachment/delete-attachment.ts @@ -2,7 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@prisma/client'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type DeleteAttachmentOptions = { id: string; @@ -14,9 +14,6 @@ export const deleteAttachment = async ({ id, userId, teamId }: DeleteAttachmentO const attachment = await prisma.envelopeAttachment.findFirst({ where: { id, - envelope: { - team: buildTeamWhereQuery({ teamId, userId }), - }, }, include: { envelope: true, @@ -29,6 +26,24 @@ export const deleteAttachment = async ({ id, userId, teamId }: DeleteAttachmentO }); } + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { type: 'envelopeId', id: attachment.envelopeId }, + userId, + teamId, + type: null, + }); + + // Additional validation to check the user has visibility-aware access to the envelope. + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Attachment not found', + }); + } + if ( attachment.envelope.status === DocumentStatus.COMPLETED || attachment.envelope.status === DocumentStatus.REJECTED diff --git a/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts index ff6402fea..1e8b1d25e 100644 --- a/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts +++ b/packages/lib/server-only/envelope-attachment/find-attachments-by-envelope-id.ts @@ -1,7 +1,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type FindAttachmentsByEnvelopeIdOptions = { envelopeId: string; @@ -14,11 +14,15 @@ export const findAttachmentsByEnvelopeId = async ({ userId, teamId, }: FindAttachmentsByEnvelopeIdOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { type: 'envelopeId', id: envelopeId }, + userId, + teamId, + type: null, + }); + const envelope = await prisma.envelope.findFirst({ - where: { - id: envelopeId, - team: buildTeamWhereQuery({ teamId, userId }), - }, + where: envelopeWhereInput, }); if (!envelope) { diff --git a/packages/lib/server-only/envelope-attachment/update-attachment.ts b/packages/lib/server-only/envelope-attachment/update-attachment.ts index c07e902a9..1758c2b60 100644 --- a/packages/lib/server-only/envelope-attachment/update-attachment.ts +++ b/packages/lib/server-only/envelope-attachment/update-attachment.ts @@ -2,7 +2,7 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@prisma/client'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type UpdateAttachmentOptions = { id: string; @@ -15,9 +15,6 @@ export const updateAttachment = async ({ id, teamId, userId, data }: UpdateAttac const attachment = await prisma.envelopeAttachment.findFirst({ where: { id, - envelope: { - team: buildTeamWhereQuery({ teamId, userId }), - }, }, include: { envelope: true, @@ -30,6 +27,24 @@ export const updateAttachment = async ({ id, teamId, userId, data }: UpdateAttac }); } + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { type: 'envelopeId', id: attachment.envelopeId }, + userId, + teamId, + type: null, + }); + + // Additional validation to check the user has visibility-aware access to the envelope. + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Attachment not found', + }); + } + if ( attachment.envelope.status === DocumentStatus.COMPLETED || attachment.envelope.status === DocumentStatus.REJECTED diff --git a/packages/lib/server-only/public-api/get-user-by-token.ts b/packages/lib/server-only/public-api/get-user-by-token.ts deleted file mode 100644 index 887730b5c..000000000 --- a/packages/lib/server-only/public-api/get-user-by-token.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { hashString } from '../auth/hash'; - -export const getUserByApiToken = async ({ token }: { token: string }) => { - const hashedToken = hashString(token); - - const user = await prisma.user.findFirst({ - where: { - apiTokens: { - some: { - token: hashedToken, - }, - }, - }, - include: { - apiTokens: true, - }, - }); - - if (!user) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'Invalid token', - statusCode: 401, - }); - } - - const retrievedToken = user.apiTokens.find((apiToken) => apiToken.token === hashedToken); - - // This should be impossible but we need to satisfy TypeScript - if (!retrievedToken) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'Invalid token', - statusCode: 401, - }); - } - - if (retrievedToken.expires && retrievedToken.expires < new Date()) { - throw new Error('Expired token'); - } - - return user; -}; diff --git a/packages/lib/server-only/recipient/get-recipient-by-id.ts b/packages/lib/server-only/recipient/get-recipient-by-id.ts index 1f0a48855..0a8df960b 100644 --- a/packages/lib/server-only/recipient/get-recipient-by-id.ts +++ b/packages/lib/server-only/recipient/get-recipient-by-id.ts @@ -4,6 +4,7 @@ import { EnvelopeType } from '@prisma/client'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../../utils/envelope'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type GetRecipientByIdOptions = { recipientId: number; @@ -41,6 +42,27 @@ export const getRecipientById = async ({ recipientId, userId, teamId, type }: Ge }); } + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: recipient.envelopeId, + }, + type, + userId, + teamId, + }); + + // Additional validation to check visibility. + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Recipient not found', + }); + } + const legacyId = { documentId: type === EnvelopeType.DOCUMENT ? mapSecondaryIdToDocumentId(recipient.envelope.secondaryId) : null, templateId: type === EnvelopeType.TEMPLATE ? mapSecondaryIdToTemplateId(recipient.envelope.secondaryId) : null, diff --git a/packages/lib/server-only/share/create-or-get-share-link.ts b/packages/lib/server-only/share/create-or-get-share-link.ts index 64842edcb..84917d74b 100644 --- a/packages/lib/server-only/share/create-or-get-share-link.ts +++ b/packages/lib/server-only/share/create-or-get-share-link.ts @@ -5,6 +5,7 @@ import { match, P } from 'ts-pattern'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { alphaid } from '../../universal/id'; import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type CreateSharingIdOptions = | { @@ -27,6 +28,7 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha ), select: { id: true, + teamId: true, }, }); @@ -46,6 +48,31 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha .then((recipient) => recipient?.email); }) .with({ userId: P.number }, async ({ userId }) => { + // Ensure the authenticated user actually has visibility-aware access to the + // envelope before allowing them to create a share link. The share route does + // not carry a teamId, so we derive it from the envelope and reuse the canonical + // visibility check (owner OR team member with sufficient visibility). + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + userId, + teamId: envelope.teamId, + type: EnvelopeType.DOCUMENT, + }); + + const accessibleEnvelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + select: { + id: true, + }, + }); + + if (!accessibleEnvelope) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + return await prisma.user .findFirst({ where: { diff --git a/packages/lib/server-only/team/get-team-public-profile.ts b/packages/lib/server-only/team/get-team-public-profile.ts index 1f0b289d0..99e46ced2 100644 --- a/packages/lib/server-only/team/get-team-public-profile.ts +++ b/packages/lib/server-only/team/get-team-public-profile.ts @@ -3,7 +3,6 @@ import type { TeamProfile } from '@prisma/client'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { buildTeamWhereQuery } from '../../utils/teams'; -import { updateTeamPublicProfile } from './update-team-public-profile'; export type GetTeamPublicProfileOptions = { userId: number; @@ -32,25 +31,24 @@ export const getTeamPublicProfile = async ({ }); } - // Create and return the public profile. + // Lazily initialize a disabled public profile on first access. Membership is + // already verified by the query above, so this system initialization does not + // impose the MANAGE_TEAM gate that updateTeamPublicProfile enforces for writes. if (!team.profile) { - const { url, profile } = await updateTeamPublicProfile({ - userId: userId, - teamId, - data: { + const profile = await prisma.teamProfile.upsert({ + where: { + teamId, + }, + create: { + teamId, enabled: false, }, + update: {}, }); - if (!profile) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Failed to create public profile', - }); - } - return { profile, - url, + url: team.url, }; } diff --git a/packages/lib/server-only/team/update-team-public-profile.ts b/packages/lib/server-only/team/update-team-public-profile.ts index ca78bd008..c39e31dbc 100644 --- a/packages/lib/server-only/team/update-team-public-profile.ts +++ b/packages/lib/server-only/team/update-team-public-profile.ts @@ -1,3 +1,4 @@ +import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams'; import { prisma } from '@documenso/prisma'; import { buildTeamWhereQuery } from '../../utils/teams'; @@ -13,7 +14,11 @@ export type UpdatePublicProfileOptions = { export const updateTeamPublicProfile = async ({ userId, teamId, data }: UpdatePublicProfileOptions) => { return await prisma.team.update({ - where: buildTeamWhereQuery({ teamId, userId }), + where: buildTeamWhereQuery({ + teamId, + userId, + roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }), data: { profile: { upsert: { diff --git a/packages/lib/server-only/user/forgot-password.ts b/packages/lib/server-only/user/forgot-password.ts index 7197f50c7..86d1c1aaa 100644 --- a/packages/lib/server-only/user/forgot-password.ts +++ b/packages/lib/server-only/user/forgot-password.ts @@ -18,31 +18,26 @@ export const forgotPassword = async ({ email }: { email: string }) => { return; } - // Find a token that was created in the last hour and hasn't expired - // const existingToken = await prisma.passwordResetToken.findFirst({ - // where: { - // userId: user.id, - // expiry: { - // gt: new Date(), - // }, - // createdAt: { - // gt: new Date(Date.now() - ONE_HOUR), - // }, - // }, - // }); - - // if (existingToken) { - // return; - // } - const token = crypto.randomBytes(18).toString('hex'); - await prisma.passwordResetToken.create({ - data: { - token, - expiry: new Date(Date.now() + ONE_HOUR), - userId: user.id, - }, + // Invalidate any prior reset tokens for this user before issuing a new one, so + // only a single token is ever live at a time. We still always issue a fresh + // token (and email) so the user can request a new link if a prior email never + // arrived, while bounding the number of usable tokens to one. + await prisma.$transaction(async (tx) => { + await tx.passwordResetToken.deleteMany({ + where: { + userId: user.id, + }, + }); + + await tx.passwordResetToken.create({ + data: { + token, + expiry: new Date(Date.now() + ONE_HOUR), + userId: user.id, + }, + }); }); await sendForgotPassword({ diff --git a/packages/lib/server-only/webhooks/zapier/subscribe.ts b/packages/lib/server-only/webhooks/zapier/subscribe.ts index 370995dd8..c05c99250 100644 --- a/packages/lib/server-only/webhooks/zapier/subscribe.ts +++ b/packages/lib/server-only/webhooks/zapier/subscribe.ts @@ -1,4 +1,6 @@ -import { AppError } from '@documenso/lib/errors/app-error'; +import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; import { assertNotPrivateUrl } from '../assert-webhook-url'; @@ -9,14 +11,36 @@ export const subscribeHandler = async (req: Request) => { const authorization = req.headers.get('authorization'); if (!authorization) { - return new Response('Unauthorized', { status: 401 }); + throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Unauthorized' }); } const { webhookUrl, eventTrigger } = await req.json(); await assertNotPrivateUrl(webhookUrl); - const result = await validateApiToken({ authorization }); + const result = await validateApiToken({ authorization }).catch(() => { + throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Unauthorized' }); + }); + + const userId = result.userId ?? result.user.id; + const teamId = result.teamId ?? undefined; + + // Re-verify the token holder still has MANAGE_TEAM on the team, mirroring the + // tRPC webhook mutations (create-webhook.ts). Guards against stale-privilege + // use of a token minted while the holder was privileged. + const team = await prisma.team.findFirst({ + where: buildTeamWhereQuery({ + teamId, + userId, + roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }), + }); + + if (!team) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You do not have permission to manage webhooks for this team', + }); + } const createdWebhook = await prisma.webhook.create({ data: { @@ -24,15 +48,19 @@ export const subscribeHandler = async (req: Request) => { eventTriggers: [eventTrigger], secret: null, enabled: true, - userId: result.userId ?? result.user.id, - teamId: result.teamId ?? undefined, + userId, + teamId, }, }); return Response.json(createdWebhook); } catch (err) { if (err instanceof AppError) { - return Response.json({ message: err.message }, { status: 400 }); + // Map authorization failures to 401, keep other AppErrors as 400 to + // preserve the existing Zapier contract (e.g. invalid webhook URL). + const status = err.code === AppErrorCode.UNAUTHORIZED ? 401 : 400; + + return Response.json({ message: err.message }, { status }); } console.error(err); diff --git a/packages/lib/server-only/webhooks/zapier/unsubscribe.ts b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts index 4772c164b..df21c0b65 100644 --- a/packages/lib/server-only/webhooks/zapier/unsubscribe.ts +++ b/packages/lib/server-only/webhooks/zapier/unsubscribe.ts @@ -1,3 +1,6 @@ +import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; import { validateApiToken } from './validateApiToken'; @@ -7,23 +10,42 @@ export const unsubscribeHandler = async (req: Request) => { const authorization = req.headers.get('authorization'); if (!authorization) { - return new Response('Unauthorized', { status: 401 }); + throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Unauthorized' }); } const { webhookId } = await req.json(); - const result = await validateApiToken({ authorization }); + const result = await validateApiToken({ authorization }).catch(() => { + throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'Unauthorized' }); + }); + const userId = result.userId ?? result.user.id; + const teamId = result.teamId ?? undefined; + + // Re-verify the token holder still has MANAGE_TEAM on the team, mirroring the + // tRPC delete-webhook-by-id mutation. Guards against stale-privilege use of a + // token minted while the holder was privileged. const deletedWebhook = await prisma.webhook.delete({ where: { id: webhookId, - userId: result.userId ?? result.user.id, - teamId: result.teamId ?? undefined, + team: buildTeamWhereQuery({ + teamId, + userId, + roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }), }, }); return Response.json(deletedWebhook); } catch (err) { + if (err instanceof AppError) { + // Map authorization failures to 401, keep other AppErrors as 400 to + // preserve the existing Zapier contract. + const status = err.code === AppErrorCode.UNAUTHORIZED ? 401 : 400; + + return Response.json({ message: err.message }, { status }); + } + console.error(err); return Response.json( diff --git a/packages/lib/utils/is-http-url.test.ts b/packages/lib/utils/is-http-url.test.ts new file mode 100644 index 000000000..3454f32eb --- /dev/null +++ b/packages/lib/utils/is-http-url.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { isHttpUrl, toSafeHref } from './is-http-url'; + +describe('isHttpUrl', () => { + it('accepts http and https URLs', () => { + expect(isHttpUrl('http://example.com')).toBe(true); + expect(isHttpUrl('https://example.com/path?q=1#hash')).toBe(true); + expect(isHttpUrl('HTTPS://EXAMPLE.COM')).toBe(true); + }); + + it('rejects non-http(s) schemes', () => { + expect(isHttpUrl('javascript:alert(1)')).toBe(false); + expect(isHttpUrl('JavaScript:alert(1)')).toBe(false); + expect(isHttpUrl('data:text/html,')).toBe(false); + expect(isHttpUrl('vbscript:msgbox(1)')).toBe(false); + expect(isHttpUrl('file:///etc/passwd')).toBe(false); + }); + + it('rejects non-absolute or unparseable values', () => { + expect(isHttpUrl('not a url')).toBe(false); + expect(isHttpUrl('/relative/path')).toBe(false); + expect(isHttpUrl('')).toBe(false); + }); + + it('does not treat leading whitespace tricks as safe', () => { + // `new URL` trims leading control chars; ensure a smuggled scheme is rejected. + expect(isHttpUrl(' javascript:alert(1)')).toBe(false); + expect(isHttpUrl('java\tscript:alert(1)')).toBe(false); + }); +}); + +describe('toSafeHref', () => { + it('returns the URL when it is http(s)', () => { + expect(toSafeHref('https://example.com')).toBe('https://example.com'); + }); + + it('returns undefined for dangerous or empty values', () => { + expect(toSafeHref('javascript:alert(1)')).toBeUndefined(); + expect(toSafeHref('data:text/html,x')).toBeUndefined(); + expect(toSafeHref('')).toBeUndefined(); + expect(toSafeHref(null)).toBeUndefined(); + expect(toSafeHref(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/lib/utils/is-http-url.ts b/packages/lib/utils/is-http-url.ts new file mode 100644 index 000000000..e5727349b --- /dev/null +++ b/packages/lib/utils/is-http-url.ts @@ -0,0 +1,32 @@ +const ALLOWED_PROTOCOLS = ['http', 'https']; + +/** + * Returns true only when `value` parses as an absolute URL using the http or + * https protocol. + * + * Zod's `.url()` accepts any parseable URL, including non-web schemes. Use this + * to restrict user-supplied URLs to http(s) before they are stored or rendered + * as a link. + */ +export const isHttpUrl = (value: string) => { + try { + const url = new URL(value); + + return ALLOWED_PROTOCOLS.includes(url.protocol.slice(0, -1).toLowerCase()); + } catch { + return false; + } +}; + +/** + * Returns the value to use for a link `href` only when it is an http(s) URL, + * otherwise `undefined`. Use this when rendering user-supplied URLs as anchors, + * including for older rows stored before URL validation was in place. + */ +export const toSafeHref = (value: string | null | undefined): string | undefined => { + if (!value || !isHttpUrl(value)) { + return undefined; + } + + return value; +}; diff --git a/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts index f07be361d..5a8484381 100644 --- a/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts +++ b/packages/trpc/server/envelope-router/attachment/create-attachment.types.ts @@ -1,3 +1,4 @@ +import { isHttpUrl } from '@documenso/lib/utils/is-http-url'; import { z } from 'zod'; import type { TrpcRouteMeta } from '../../trpc'; @@ -16,7 +17,7 @@ export const ZCreateAttachmentRequestSchema = z.object({ envelopeId: z.string(), data: z.object({ label: z.string().min(1, 'Label is required'), - data: z.string().url('Must be a valid URL'), + data: z.string().url('Must be a valid URL').refine(isHttpUrl, 'URL must use the http or https protocol'), }), }); diff --git a/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts index 2acff8257..2f60e1fc2 100644 --- a/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts +++ b/packages/trpc/server/envelope-router/attachment/update-attachment.types.ts @@ -1,3 +1,4 @@ +import { isHttpUrl } from '@documenso/lib/utils/is-http-url'; import { z } from 'zod'; import { ZSuccessResponseSchema } from '../../schema'; @@ -17,7 +18,7 @@ export const ZUpdateAttachmentRequestSchema = z.object({ id: z.string(), data: z.object({ label: z.string().min(1, 'Label is required'), - data: z.string().url('Must be a valid URL'), + data: z.string().url('Must be a valid URL').refine(isHttpUrl, 'URL must use the http or https protocol'), }), }); diff --git a/packages/trpc/server/envelope-router/envelope-recipients/get-envelope-recipient.ts b/packages/trpc/server/envelope-router/envelope-recipients/get-envelope-recipient.ts index 7ef3e8bc6..4372b9291 100644 --- a/packages/trpc/server/envelope-router/envelope-recipients/get-envelope-recipient.ts +++ b/packages/trpc/server/envelope-router/envelope-recipients/get-envelope-recipient.ts @@ -1,4 +1,5 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; @@ -41,5 +42,26 @@ export const getEnvelopeRecipientRoute = authenticatedProcedure }); } + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: recipient.envelopeId, + }, + type: null, + userId: user.id, + teamId, + }); + + // Additional validation to check visibility. + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Recipient not found', + }); + } + return recipient; }); diff --git a/packages/trpc/server/organisation-router/update-organisation-group.ts b/packages/trpc/server/organisation-router/update-organisation-group.ts index 06d45d763..c5be77bd2 100644 --- a/packages/trpc/server/organisation-router/update-organisation-group.ts +++ b/packages/trpc/server/organisation-router/update-organisation-group.ts @@ -38,6 +38,15 @@ export const updateOrganisationGroupRoute = authenticatedProcedure }, include: { organisationGroupMembers: true, + organisation: { + include: { + members: { + select: { + id: true, + }, + }, + }, + }, }, }); @@ -78,6 +87,15 @@ export const updateOrganisationGroupRoute = authenticatedProcedure const groupMemberIds = unique(data.memberIds || []); + // Validate that members belong to the same organisation as the group. + groupMemberIds.forEach((memberId) => { + const member = organisationGroup.organisation.members.find(({ id }) => id === memberId); + + if (!member) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + }); + const membersToDelete = organisationGroup.organisationGroupMembers.filter( (member) => !groupMemberIds.includes(member.organisationMemberId), ); From 1f170ef5e543480d547ab34c73b3ddd76cf90062 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 29 Jun 2026 14:11:31 +1000 Subject: [PATCH 12/41] fix: scope organisation group deletion (#3047) --- packages/lib/server-only/team/delete-team.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/server-only/team/delete-team.ts b/packages/lib/server-only/team/delete-team.ts index 182a004a0..b9393db84 100644 --- a/packages/lib/server-only/team/delete-team.ts +++ b/packages/lib/server-only/team/delete-team.ts @@ -85,6 +85,7 @@ export const deleteTeam = async ({ userId, teamId }: DeleteTeamOptions) => { // Purge all internal organisation groups that have no teams. await tx.organisationGroup.deleteMany({ where: { + organisationId: team.organisationId, type: OrganisationGroupType.INTERNAL_TEAM, teamGroups: { none: {}, From a70b0702c3ac765b0126611f55bf2dc3913fe5b9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Mon, 29 Jun 2026 14:50:35 +1000 Subject: [PATCH 13/41] fix: add missing teams branding guard (#3049) --- .../t.$teamUrl+/settings.branding.tsx | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx index 287a91b8b..b674dc460 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx @@ -1,14 +1,17 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations'; import type { SanitizeBrandingCssWarning } from '@documenso/lib/utils/sanitize-branding-css'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { plural } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; import { Loader } from 'lucide-react'; import { useState } from 'react'; +import { Link } from 'react-router'; import { BrandingPreferencesForm, @@ -36,6 +39,8 @@ export default function TeamsSettingsPage() { const { mutateAsync: updateTeamSettings } = trpc.team.settings.update.useMutation(); + const canConfigureBranding = organisation.organisationClaim.flags.allowCustomBranding || !IS_BILLING_ENABLED(); + const canCustomBranding = organisation.organisationClaim.flags.embedSigningWhiteLabel === true || !IS_BILLING_ENABLED(); @@ -112,39 +117,61 @@ export default function TeamsSettingsPage() { subtitle={t`Here you can set preferences and defaults for branding.`} /> -
- + {canConfigureBranding ? ( +
+ - {cssWarnings.length > 0 && ( - + {cssWarnings.length > 0 && ( + + + CSS rules were dropped during sanitisation + + + +
    + {cssWarnings.map((warning, index) => ( +
  • + {warning.detail} + {warning.line !== undefined && ( + + {' '} + (line {warning.line}) + + )} +
  • + ))} +
+
+
+ )} +
+ ) : ( + +
- CSS rules were dropped during sanitisation + Branding Preferences - -
    - {cssWarnings.map((warning, index) => ( -
  • - {warning.detail} - {warning.line !== undefined && ( - - {' '} - (line {warning.line}) - - )} -
  • - ))} -
+ + Currently branding can only be configured for Teams and above plans. - - )} -
+
+ + {canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole) && ( + + )} + + )}
); } From 9cdd2e7ff90e59aa6f7a0cd997e0b528d938a023 Mon Sep 17 00:00:00 2001 From: Martin Glaser <65476570+martindglaser@users.noreply.github.com> Date: Mon, 29 Jun 2026 03:07:43 -0300 Subject: [PATCH 14/41] fix(email): render Preview inside Body across all email templates (#3004) --- packages/email/templates/access-auth-2fa.tsx | 3 ++- packages/email/templates/admin-user-created.tsx | 3 ++- packages/email/templates/bulk-send-complete.tsx | 5 ++++- packages/email/templates/confirm-email.tsx | 3 ++- packages/email/templates/confirm-team-email.tsx | 4 ++-- packages/email/templates/document-cancel.tsx | 3 ++- packages/email/templates/document-completed.tsx | 4 ++-- .../templates/document-created-from-direct-template.tsx | 4 ++-- packages/email/templates/document-invite.tsx | 3 ++- packages/email/templates/document-pending.tsx | 4 ++-- packages/email/templates/document-recipient-signed.tsx | 4 ++-- packages/email/templates/document-rejected.tsx | 3 ++- packages/email/templates/document-rejection-confirmed.tsx | 3 ++- packages/email/templates/document-reminder.tsx | 3 ++- packages/email/templates/document-self-signed.tsx | 4 ++-- packages/email/templates/document-super-delete.tsx | 3 ++- packages/email/templates/forgot-password.tsx | 3 ++- .../templates/organisation-account-link-confirmation.tsx | 3 ++- packages/email/templates/organisation-delete.tsx | 4 ++-- packages/email/templates/organisation-invite.tsx | 4 ++-- packages/email/templates/organisation-join.tsx | 4 ++-- packages/email/templates/organisation-leave.tsx | 4 ++-- packages/email/templates/organisation-limit-alert.tsx | 4 ++-- packages/email/templates/recipient-expired.tsx | 3 ++- packages/email/templates/recipient-removed-from-document.tsx | 3 ++- packages/email/templates/reset-password.tsx | 3 ++- packages/email/templates/team-delete.tsx | 4 ++-- packages/email/templates/team-email-removed.tsx | 4 ++-- 28 files changed, 58 insertions(+), 41 deletions(-) diff --git a/packages/email/templates/access-auth-2fa.tsx b/packages/email/templates/access-auth-2fa.tsx index ecff50224..adc66f65a 100644 --- a/packages/email/templates/access-auth-2fa.tsx +++ b/packages/email/templates/access-auth-2fa.tsx @@ -30,9 +30,10 @@ export const AccessAuth2FAEmailTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/admin-user-created.tsx b/packages/email/templates/admin-user-created.tsx index f82ce4605..ccc92020b 100644 --- a/packages/email/templates/admin-user-created.tsx +++ b/packages/email/templates/admin-user-created.tsx @@ -21,8 +21,9 @@ export const AdminUserCreatedTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/bulk-send-complete.tsx b/packages/email/templates/bulk-send-complete.tsx index 8b0bf3b45..cc15ad0c3 100644 --- a/packages/email/templates/bulk-send-complete.tsx +++ b/packages/email/templates/bulk-send-complete.tsx @@ -24,11 +24,14 @@ export const BulkSendCompleteEmail = ({ }: BulkSendCompleteEmailProps) => { const { _ } = useLingui(); + const previewText = msg`Bulk send operation complete for template "${templateName}"`; + return ( - {_(msg`Bulk send operation complete for template "${templateName}"`)} + {_(previewText)} +
diff --git a/packages/email/templates/confirm-email.tsx b/packages/email/templates/confirm-email.tsx index 5024b28b0..f57dcdec9 100644 --- a/packages/email/templates/confirm-email.tsx +++ b/packages/email/templates/confirm-email.tsx @@ -18,8 +18,9 @@ export const ConfirmEmailTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/confirm-team-email.tsx b/packages/email/templates/confirm-team-email.tsx index dfb760d35..6deab57de 100644 --- a/packages/email/templates/confirm-team-email.tsx +++ b/packages/email/templates/confirm-team-email.tsx @@ -30,9 +30,9 @@ export const ConfirmTeamEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-cancel.tsx b/packages/email/templates/document-cancel.tsx index f2eb84c6e..34c077252 100644 --- a/packages/email/templates/document-cancel.tsx +++ b/packages/email/templates/document-cancel.tsx @@ -23,9 +23,10 @@ export const DocumentCancelTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/document-completed.tsx b/packages/email/templates/document-completed.tsx index bc54748aa..b0cff91d3 100644 --- a/packages/email/templates/document-completed.tsx +++ b/packages/email/templates/document-completed.tsx @@ -26,9 +26,9 @@ export const DocumentCompletedEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-created-from-direct-template.tsx b/packages/email/templates/document-created-from-direct-template.tsx index 4ae9aad98..5b8b12abc 100644 --- a/packages/email/templates/document-created-from-direct-template.tsx +++ b/packages/email/templates/document-created-from-direct-template.tsx @@ -33,9 +33,9 @@ export const DocumentCreatedFromDirectTemplateEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-invite.tsx b/packages/email/templates/document-invite.tsx index 277f28859..a876680c6 100644 --- a/packages/email/templates/document-invite.tsx +++ b/packages/email/templates/document-invite.tsx @@ -56,9 +56,10 @@ export const DocumentInviteEmailTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/document-pending.tsx b/packages/email/templates/document-pending.tsx index a6f91b60a..f0879b080 100644 --- a/packages/email/templates/document-pending.tsx +++ b/packages/email/templates/document-pending.tsx @@ -20,9 +20,9 @@ export const DocumentPendingEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-recipient-signed.tsx b/packages/email/templates/document-recipient-signed.tsx index df7fed7b8..af125f628 100644 --- a/packages/email/templates/document-recipient-signed.tsx +++ b/packages/email/templates/document-recipient-signed.tsx @@ -28,9 +28,9 @@ export const DocumentRecipientSignedEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-rejected.tsx b/packages/email/templates/document-rejected.tsx index c17348eba..3e343dbb3 100644 --- a/packages/email/templates/document-rejected.tsx +++ b/packages/email/templates/document-rejected.tsx @@ -28,9 +28,10 @@ export function DocumentRejectedEmail({ return ( - {previewText} + {previewText} +
diff --git a/packages/email/templates/document-rejection-confirmed.tsx b/packages/email/templates/document-rejection-confirmed.tsx index 383417e20..e88e31d59 100644 --- a/packages/email/templates/document-rejection-confirmed.tsx +++ b/packages/email/templates/document-rejection-confirmed.tsx @@ -28,9 +28,10 @@ export function DocumentRejectionConfirmedEmail({ return ( - {previewText} + {previewText} +
diff --git a/packages/email/templates/document-reminder.tsx b/packages/email/templates/document-reminder.tsx index 5633b4959..c5759b48d 100644 --- a/packages/email/templates/document-reminder.tsx +++ b/packages/email/templates/document-reminder.tsx @@ -37,9 +37,10 @@ export const DocumentReminderEmailTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/document-self-signed.tsx b/packages/email/templates/document-self-signed.tsx index 3d8657f2d..7c848ff99 100644 --- a/packages/email/templates/document-self-signed.tsx +++ b/packages/email/templates/document-self-signed.tsx @@ -20,9 +20,9 @@ export const DocumentSelfSignedEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/document-super-delete.tsx b/packages/email/templates/document-super-delete.tsx index d0098bf7d..03cb0b0a9 100644 --- a/packages/email/templates/document-super-delete.tsx +++ b/packages/email/templates/document-super-delete.tsx @@ -23,9 +23,10 @@ export const DocumentSuperDeleteEmailTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/forgot-password.tsx b/packages/email/templates/forgot-password.tsx index 16652b99e..93620700a 100644 --- a/packages/email/templates/forgot-password.tsx +++ b/packages/email/templates/forgot-password.tsx @@ -20,9 +20,10 @@ export const ForgotPasswordTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/organisation-account-link-confirmation.tsx b/packages/email/templates/organisation-account-link-confirmation.tsx index 2c66b86fa..aca38b491 100644 --- a/packages/email/templates/organisation-account-link-confirmation.tsx +++ b/packages/email/templates/organisation-account-link-confirmation.tsx @@ -30,8 +30,9 @@ export const OrganisationAccountLinkConfirmationTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/organisation-delete.tsx b/packages/email/templates/organisation-delete.tsx index d78a6a211..81937870c 100644 --- a/packages/email/templates/organisation-delete.tsx +++ b/packages/email/templates/organisation-delete.tsx @@ -34,9 +34,9 @@ export const OrganisationDeleteEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/organisation-invite.tsx b/packages/email/templates/organisation-invite.tsx index 14322662b..09961a4d1 100644 --- a/packages/email/templates/organisation-invite.tsx +++ b/packages/email/templates/organisation-invite.tsx @@ -29,9 +29,9 @@ export const OrganisationInviteEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/organisation-join.tsx b/packages/email/templates/organisation-join.tsx index 6356aa090..eb676b5d3 100644 --- a/packages/email/templates/organisation-join.tsx +++ b/packages/email/templates/organisation-join.tsx @@ -31,9 +31,9 @@ export const OrganisationJoinEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/organisation-leave.tsx b/packages/email/templates/organisation-leave.tsx index f2d1b6313..058b830cb 100644 --- a/packages/email/templates/organisation-leave.tsx +++ b/packages/email/templates/organisation-leave.tsx @@ -31,9 +31,9 @@ export const OrganisationLeaveEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/organisation-limit-alert.tsx b/packages/email/templates/organisation-limit-alert.tsx index 536710d29..5cd04001a 100644 --- a/packages/email/templates/organisation-limit-alert.tsx +++ b/packages/email/templates/organisation-limit-alert.tsx @@ -29,9 +29,9 @@ export const OrganisationLimitAlertEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/recipient-expired.tsx b/packages/email/templates/recipient-expired.tsx index 0592089cc..71a186462 100644 --- a/packages/email/templates/recipient-expired.tsx +++ b/packages/email/templates/recipient-expired.tsx @@ -23,9 +23,10 @@ export const RecipientExpiredTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/recipient-removed-from-document.tsx b/packages/email/templates/recipient-removed-from-document.tsx index a495edbb8..5035c9679 100644 --- a/packages/email/templates/recipient-removed-from-document.tsx +++ b/packages/email/templates/recipient-removed-from-document.tsx @@ -22,9 +22,10 @@ export const RecipientRemovedFromDocumentTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/reset-password.tsx b/packages/email/templates/reset-password.tsx index 3c4a67c8f..19c5979a3 100644 --- a/packages/email/templates/reset-password.tsx +++ b/packages/email/templates/reset-password.tsx @@ -22,9 +22,10 @@ export const ResetPasswordTemplate = ({ return ( - {_(previewText)} + {_(previewText)} +
diff --git a/packages/email/templates/team-delete.tsx b/packages/email/templates/team-delete.tsx index 336b46d62..59ca46243 100644 --- a/packages/email/templates/team-delete.tsx +++ b/packages/email/templates/team-delete.tsx @@ -29,9 +29,9 @@ export const TeamDeleteEmailTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
diff --git a/packages/email/templates/team-email-removed.tsx b/packages/email/templates/team-email-removed.tsx index 0da2b3554..7543c8789 100644 --- a/packages/email/templates/team-email-removed.tsx +++ b/packages/email/templates/team-email-removed.tsx @@ -30,9 +30,9 @@ export const TeamEmailRemovedTemplate = ({ return ( - {_(previewText)} - + {_(previewText)} +
From 7062fadf0b8f533c8b8836ec57edcbb88fb2d086 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 30 Jun 2026 15:45:48 +1000 Subject: [PATCH 15/41] fix: add additional team group permission checks (#3052) --- .../app-tests/e2e/teams/team-groups.spec.ts | 163 ++++++++++++++++++ .../server/team-router/delete-team-group.ts | 9 + .../server/team-router/update-team-group.ts | 7 +- 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 packages/app-tests/e2e/teams/team-groups.spec.ts diff --git a/packages/app-tests/e2e/teams/team-groups.spec.ts b/packages/app-tests/e2e/teams/team-groups.spec.ts new file mode 100644 index 000000000..74c7f6372 --- /dev/null +++ b/packages/app-tests/e2e/teams/team-groups.spec.ts @@ -0,0 +1,163 @@ +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { generateDatabaseId } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { seedTeamMember } from '@documenso/prisma/seed/teams'; +import { seedUser } from '@documenso/prisma/seed/users'; +import { expect, type Page, test } from '@playwright/test'; +import { OrganisationGroupType, OrganisationMemberRole, TeamMemberRole } from '@prisma/client'; + +import { apiSignin } from '../fixtures/authentication'; + +const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); + +test.describe.configure({ mode: 'parallel' }); + +/** + * Calls a team-group tRPC mutation directly, bypassing the UI. + * + * The UI only ever surfaces CUSTOM / INTERNAL_ORGANISATION groups, so these + * authorisation rules must be enforced on the server - a crafted request can + * target any `teamGroupId`, including the system-managed INTERNAL_TEAM groups. + */ +const callTeamGroupMutation = ( + page: Page, + procedure: 'team.group.delete' | 'team.group.update', + teamId: number, + input: Record, +) => + page.context().request.post(`${WEBAPP_BASE_URL}/api/trpc/${procedure}`, { + headers: { 'content-type': 'application/json', 'x-team-id': teamId.toString() }, + data: JSON.stringify({ json: input }), + }); + +/** + * Every team is created with three system-managed INTERNAL_TEAM groups + * (admin/manager/member). They are the backbone of team-specific access and, + * like organisation internal groups, must not be deletable - deleting them + * silently strips team members of access while leaving the team row in place. + */ +test('[TEAMS]: internal team groups cannot be deleted via the API', async ({ page }) => { + // Member inheritance OFF: membership is granted exclusively through the team's + // INTERNAL_TEAM groups, so removing them is what causes the access loss. + const { user: owner, team } = await seedUser({ inheritMembers: false }); + + // A direct team member whose access depends on the INTERNAL_TEAM member group. + const directMember = await seedTeamMember({ teamId: team.id, role: TeamMemberRole.MEMBER }); + + await apiSignin({ page, email: owner.email }); + + const internalTeamGroups = await prisma.teamGroup.findMany({ + where: { + teamId: team.id, + organisationGroup: { type: OrganisationGroupType.INTERNAL_TEAM }, + }, + }); + + // admin + manager + member. + expect(internalTeamGroups).toHaveLength(3); + + for (const group of internalTeamGroups) { + const response = await callTeamGroupMutation(page, 'team.group.delete', team.id, { + teamId: team.id, + teamGroupId: group.id, + }); + + expect(response.status(), `INTERNAL_TEAM ${group.teamRole} group must not be deletable`).not.toBe(200); + } + + // None of the internal groups were removed. + const remaining = await prisma.teamGroup.count({ + where: { + teamId: team.id, + organisationGroup: { type: OrganisationGroupType.INTERNAL_TEAM }, + }, + }); + + expect(remaining).toBe(3); + + // The direct member therefore keeps their team access. + const memberStillHasAccess = await prisma.teamGroup.findFirst({ + where: { + teamId: team.id, + organisationGroup: { + type: OrganisationGroupType.INTERNAL_TEAM, + organisationGroupMembers: { + some: { organisationMember: { userId: directMember.id } }, + }, + }, + }, + }); + + expect(memberStillHasAccess).not.toBeNull(); +}); + +/** + * Guards against over-blocking: user-created (CUSTOM) team groups are not + * internal and must remain removable by team managers/admins. + */ +test('[TEAMS]: custom team groups can still be deleted', async ({ page }) => { + const { user: owner, organisation, team } = await seedUser({ inheritMembers: false }); + + const customGroup = await prisma.organisationGroup.create({ + data: { + id: generateDatabaseId('org_group'), + name: `custom-${team.url}`, + type: OrganisationGroupType.CUSTOM, + organisationRole: OrganisationMemberRole.MEMBER, + organisationId: organisation.id, + teamGroups: { + create: { + id: generateDatabaseId('team_group'), + teamId: team.id, + teamRole: TeamMemberRole.MEMBER, + }, + }, + }, + include: { teamGroups: true }, + }); + + const customTeamGroup = customGroup.teamGroups[0]; + + await apiSignin({ page, email: owner.email }); + + const response = await callTeamGroupMutation(page, 'team.group.delete', team.id, { + teamId: team.id, + teamGroupId: customTeamGroup.id, + }); + + expect(response.status()).toBe(200); + + const deleted = await prisma.teamGroup.findUnique({ where: { id: customTeamGroup.id } }); + + expect(deleted).toBeNull(); +}); + +/** + * The same root cause affects updates: an INTERNAL_TEAM group's role must not be + * editable either, otherwise a team admin could rewrite the backbone roles + * (e.g. promote the member group to admin). + */ +test('[TEAMS]: internal team groups cannot be updated via the API', async ({ page }) => { + const { user: owner, team } = await seedUser({ inheritMembers: false }); + + await apiSignin({ page, email: owner.email }); + + const internalMemberGroup = await prisma.teamGroup.findFirstOrThrow({ + where: { + teamId: team.id, + teamRole: TeamMemberRole.MEMBER, + organisationGroup: { type: OrganisationGroupType.INTERNAL_TEAM }, + }, + }); + + const response = await callTeamGroupMutation(page, 'team.group.update', team.id, { + id: internalMemberGroup.id, + data: { teamRole: TeamMemberRole.ADMIN }, + }); + + expect(response.status()).not.toBe(200); + + const reloaded = await prisma.teamGroup.findUniqueOrThrow({ where: { id: internalMemberGroup.id } }); + + expect(reloaded.teamRole).toBe(TeamMemberRole.MEMBER); +}); diff --git a/packages/trpc/server/team-router/delete-team-group.ts b/packages/trpc/server/team-router/delete-team-group.ts index a3953f4c1..50a3cc68b 100644 --- a/packages/trpc/server/team-router/delete-team-group.ts +++ b/packages/trpc/server/team-router/delete-team-group.ts @@ -53,6 +53,15 @@ export const deleteTeamGroupRoute = authenticatedProcedure }); } + // You cannot delete internal team groups. These are the system-managed + // admin/manager/member groups that back the team's role-based access, and + // deleting them would silently strip team members of their access. + if (group.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You are not allowed to delete internal team groups', + }); + } + // You cannot delete internal organisation groups. // The only exception is deleting the "member" organisation group which is used to allow // all organisation members to access a team. diff --git a/packages/trpc/server/team-router/update-team-group.ts b/packages/trpc/server/team-router/update-team-group.ts index f348d49fb..cd11f92d2 100644 --- a/packages/trpc/server/team-router/update-team-group.ts +++ b/packages/trpc/server/team-router/update-team-group.ts @@ -45,9 +45,12 @@ export const updateTeamGroupRoute = authenticatedProcedure }); } - if (teamGroup.organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION) { + if ( + teamGroup.organisationGroup.type === OrganisationGroupType.INTERNAL_ORGANISATION || + teamGroup.organisationGroup.type === OrganisationGroupType.INTERNAL_TEAM + ) { throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'You are not allowed to update internal organisation groups', + message: 'You are not allowed to update internal groups', }); } From 3b110cf70de23bb9aad0f6ec7737626ed8d64360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Chevalier?= <158556490+GregCnpp@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:52:59 +0200 Subject: [PATCH 16/41] fix: french translation for confirmation message (#3050) --- packages/lib/translations/fr/web.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index 1305373bc..181b21a64 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -8424,7 +8424,7 @@ msgstr "Veuillez réessayer ou contacter notre support." #. placeholder {0}: `'${t(deleteMessage)}'` #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "Please type {0} to confirm" -msgstr "Veuiillez taper {0} pour confirmer" +msgstr "Veuillez taper {0} pour confirmer" #. placeholder {0}: user.email #: apps/remix/app/components/dialogs/account-delete-dialog.tsx From 562d78e2d7f20db0f1d5dc63375379b3af0d07c5 Mon Sep 17 00:00:00 2001 From: Kendry Grullon Date: Tue, 30 Jun 2026 02:08:09 -0400 Subject: [PATCH 17/41] feat: add granular signin disable flags and OIDC auto-redirect (#2857) --- .env.example | 14 +++ .../configuration/environment.mdx | 54 +++++++++ .../deployment/docker-compose.mdx | 13 ++ .../docs/self-hosting/deployment/docker.mdx | 6 + .../docs/self-hosting/deployment/railway.mdx | 6 + apps/remix/app/components/forms/signin.tsx | 112 ++++++++++-------- .../_unauthenticated+/forgot-password.tsx | 11 +- .../reset-password.$token.tsx | 5 + .../reset-password._index.tsx | 11 +- .../app/routes/_unauthenticated+/signin.tsx | 56 ++++++++- docker/production/compose.yml | 6 + .../auth/server/lib/errors/error-codes.ts | 1 + packages/auth/server/routes/email-password.ts | 25 ++++ packages/lib/constants/auth.ts | 27 +++++ packages/lib/translations/de/web.po | 5 + packages/lib/translations/en/web.po | 5 + packages/lib/translations/es/web.po | 5 + packages/lib/translations/fr/web.po | 5 + packages/lib/translations/it/web.po | 5 + packages/lib/translations/ja/web.po | 5 + packages/lib/translations/ko/web.po | 5 + packages/lib/translations/nl/web.po | 5 + packages/lib/translations/pl/web.po | 5 + packages/lib/translations/pt-BR/web.po | 5 + packages/lib/translations/zh/web.po | 5 + packages/tsconfig/process-env.d.ts | 7 ++ render.yaml | 12 ++ turbo.json | 6 + 28 files changed, 371 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index be3ecaab3..5f3da7c1f 100644 --- a/.env.example +++ b/.env.example @@ -180,6 +180,20 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP= NEXT_PUBLIC_DISABLE_OIDC_SIGNUP= # OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org). NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS= +# OPTIONAL: Set to "true" to disable all signin methods (email, Google, Microsoft, OIDC). +NEXT_PUBLIC_DISABLE_SIGNIN= +# OPTIONAL: Set to "true" to disable email/password signin only. Also closes /forgot-password and /reset-password. +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN= +# OPTIONAL: Set to "true" to hide the Google signin button. +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN= +# OPTIONAL: Set to "true" to hide the Microsoft signin button. +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN= +# OPTIONAL: Set to "true" to hide the OIDC signin button. +NEXT_PUBLIC_DISABLE_OIDC_SIGNIN= +# OPTIONAL: When OIDC is the only enabled signin transport, /signin auto-redirects +# 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: Set to true to use internal webapp url in browserless requests. NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false diff --git a/apps/docs/content/docs/self-hosting/configuration/environment.mdx b/apps/docs/content/docs/self-hosting/configuration/environment.mdx index 4ec25e213..4b910b43b 100644 --- a/apps/docs/content/docs/self-hosting/configuration/environment.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/environment.mdx @@ -272,6 +272,12 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch. Disable all signin methods application-wide | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin. Also closes `/forgot-password` and `/reset-password` | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable the automatic `/signin` redirect when OIDC is the only enabled transport | `false` | | `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | | | `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` | @@ -303,6 +309,44 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" NEXT_PUBLIC_DISABLE_SIGNUP="true" ``` +### Sign-in Restrictions + +You can control which methods are available for users to sign in with the following environment variables: + +- **`NEXT_PUBLIC_DISABLE_SIGNIN`** (master switch): Set to `true` to block all signin methods (email/password, Google, Microsoft, OIDC). Hides every signin entry point on `/signin` and rejects email/password signin server-side with a `SIGNIN_DISABLED` error. +- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN`**: Set to `true` to disable email/password signin only. The email/password form is hidden, the `/forgot-password` and `/reset-password` pages redirect to `/signin`, and the corresponding server endpoints reject requests. SSO signin is unaffected. +- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNIN`**: Set to `true` to hide the matching SSO button on the signin page. Useful when an SSO provider is kept configured for account linking but not advertised as a signin entry point. + +These flags are opt-in: when none are set, signin behaviour is unchanged from a stock Documenso instance. + +```bash +# Allow only OIDC signin (e.g. enterprise SSO-only) +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" + +# Or disable signin entirely +NEXT_PUBLIC_DISABLE_SIGNIN="true" +``` + +### OIDC Auto-redirect + +When OIDC is the only enabled signin transport on your instance, `/signin` automatically redirects users straight to the OIDC provider instead of showing the signin form. The page renders a spinner while the redirect happens. No extra configuration is required — disabling every other signin method is enough to trigger it. + +- **`NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT`**: Set to `true` to opt out of the automatic redirect and keep rendering the signin page even when OIDC is the only enabled transport. + +The redirect only triggers when OIDC is configured and email/password, Google, and Microsoft signin are all disabled. If any other transport remains enabled, the signin form is shown as normal. + +```bash +# OIDC-only signin: disabling all other methods auto-redirects to the provider +NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" + +# Opt out of the auto-redirect while still OIDC-only +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" +``` + --- ## AI Features @@ -446,6 +490,16 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true" # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true" # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org" + +# Sign-in restrictions (optional) +# NEXT_PUBLIC_DISABLE_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true" +# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN="true" + +# Opt out of the automatic OIDC redirect when OIDC is the only enabled transport (optional) +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true" ``` --- diff --git a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx index b3590a7f2..84e228115 100644 --- a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx @@ -163,6 +163,19 @@ NEXT_PUBLIC_DISABLE_SIGNUP=false # NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true # NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true # NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org + +# Signin restrictions (optional) +# Master switch — disables every signin method +# NEXT_PUBLIC_DISABLE_SIGNIN=true +# Per-method switches (optional). Each disables that signin path. +# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true +# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=true +# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=true +# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=true + +# When OIDC is the only enabled transport, /signin auto-redirects to the provider. +# Set this to opt out and keep showing the signin page (optional). +# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true ``` Generate secure secrets using: `openssl rand -base64 32` diff --git a/apps/docs/content/docs/self-hosting/deployment/docker.mdx b/apps/docs/content/docs/self-hosting/deployment/docker.mdx index 7d6d4b9cd..68508e767 100644 --- a/apps/docs/content/docs/self-hosting/deployment/docker.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/docker.mdx @@ -112,6 +112,12 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment). diff --git a/apps/docs/content/docs/self-hosting/deployment/railway.mdx b/apps/docs/content/docs/self-hosting/deployment/railway.mdx index 81392a37d..501edca1c 100644 --- a/apps/docs/content/docs/self-hosting/deployment/railway.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/railway.mdx @@ -159,6 +159,12 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com | `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` | | `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` | | `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | | +| `NEXT_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` | +| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` | +| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` | +| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`| Hide the Microsoft signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` | +| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` | | `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - | | `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` | diff --git a/apps/remix/app/components/forms/signin.tsx b/apps/remix/app/components/forms/signin.tsx index 09e72d7b2..16b9943ca 100644 --- a/apps/remix/app/components/forms/signin.tsx +++ b/apps/remix/app/components/forms/signin.tsx @@ -58,6 +58,7 @@ export type TSignInFormSchema = z.infer; export type SignInFormProps = { className?: string; initialEmail?: string; + isEmailPasswordSigninEnabled?: boolean; isGoogleSSOEnabled?: boolean; isMicrosoftSSOEnabled?: boolean; isOIDCSSOEnabled?: boolean; @@ -68,6 +69,7 @@ export type SignInFormProps = { export const SignInForm = ({ className, initialEmail, + isEmailPasswordSigninEnabled = true, isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, @@ -324,66 +326,78 @@ export const SignInForm = ({
- ( - - - Email - + {isEmailPasswordSigninEnabled && ( + <> + ( + + + Email + - - - + + + - - - )} - /> + + + )} + /> - ( - - - Password - + ( + + + Password + - - - + + + - + -

- - Forgot your password? - -

-
- )} - /> +

+ + Forgot your password? + +

+
+ )} + /> - {turnstileSiteKey && !isTwoFactorAuthenticationDialogOpen && ( - + {turnstileSiteKey && !isTwoFactorAuthenticationDialogOpen && ( + + )} + + + )} - - {!isEmbeddedRedirect && ( <> - {hasSocialAuthEnabled && ( + {isEmailPasswordSigninEnabled && hasSocialAuthEnabled && (
diff --git a/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx b/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx index 68d721c83..c47d0c256 100644 --- a/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx +++ b/apps/remix/app/routes/_unauthenticated+/forgot-password.tsx @@ -1,6 +1,7 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { Link } from 'react-router'; +import { Link, redirect } from 'react-router'; import { ForgotPasswordForm } from '~/components/forms/forgot-password'; import { appMetaTags } from '~/utils/meta'; @@ -9,6 +10,14 @@ export function meta() { return appMetaTags(msg`Forgot Password`); } +export async function loader() { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + + return null; +} + export default function ForgotPasswordPage() { return (
diff --git a/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx b/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx index 279b68426..0d9ebfe8b 100644 --- a/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx +++ b/apps/remix/app/routes/_unauthenticated+/reset-password.$token.tsx @@ -1,3 +1,4 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { getResetTokenValidity } from '@documenso/lib/server-only/user/get-reset-token-validity'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; @@ -13,6 +14,10 @@ export function meta() { } export async function loader({ params }: Route.LoaderArgs) { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + const { token } = params; const isValid = await getResetTokenValidity({ token }); diff --git a/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx b/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx index 6e25d11d7..83ed01042 100644 --- a/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx +++ b/apps/remix/app/routes/_unauthenticated+/reset-password._index.tsx @@ -1,7 +1,8 @@ +import { isSigninEnabledForProvider } from '@documenso/lib/constants/auth'; import { Button } from '@documenso/ui/primitives/button'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import { Link } from 'react-router'; +import { Link, redirect } from 'react-router'; import { appMetaTags } from '~/utils/meta'; @@ -9,6 +10,14 @@ export function meta() { return appMetaTags(msg`Reset Password`); } +export async function loader() { + if (!isSigninEnabledForProvider('email')) { + throw redirect('/signin'); + } + + return null; +} + export default function ResetPasswordPage() { return (
diff --git a/apps/remix/app/routes/_unauthenticated+/signin.tsx b/apps/remix/app/routes/_unauthenticated+/signin.tsx index 5ea5a5398..4f9920fc3 100644 --- a/apps/remix/app/routes/_unauthenticated+/signin.tsx +++ b/apps/remix/app/routes/_unauthenticated+/signin.tsx @@ -1,8 +1,11 @@ +import { authClient } from '@documenso/auth/client'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { IS_GOOGLE_SSO_ENABLED, IS_MICROSOFT_SSO_ENABLED, + IS_OIDC_AUTO_REDIRECT_DISABLED, IS_OIDC_SSO_ENABLED, + isSigninEnabledForProvider, isSignupEnabledForProvider, OIDC_PROVIDER_LABEL, } from '@documenso/lib/constants/auth'; @@ -11,6 +14,7 @@ import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; +import { Loader2Icon } from 'lucide-react'; import { useEffect, useState } from 'react'; import { Link, redirect, useSearchParams } from 'react-router'; @@ -28,10 +32,20 @@ export async function loader({ request }: Route.LoaderArgs) { const { isAuthenticated } = await getOptionalSession(request); // SSR env variables. - const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED; - const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED; - const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED; + const isEmailPasswordSigninEnabled = isSigninEnabledForProvider('email'); + const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED && isSigninEnabledForProvider('google'); + const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED && isSigninEnabledForProvider('microsoft'); + const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED && isSigninEnabledForProvider('oidc'); + + // Automatically redirect to OIDC when it is the only enabled signin transport, + // unless the redirect has been explicitly disabled via env. + const isOIDCOnlyTransport = + isOIDCSSOEnabled && !isEmailPasswordSigninEnabled && !isGoogleSSOEnabled && !isMicrosoftSSOEnabled; + + const shouldAutoRedirectToOIDC = isOIDCOnlyTransport && !IS_OIDC_AUTO_REDIRECT_DISABLED; + const oidcProviderLabel = OIDC_PROVIDER_LABEL; + const isSignupEnabled = isSignupEnabledForProvider('email') || (IS_GOOGLE_SSO_ENABLED && isSignupEnabledForProvider('google')) || @@ -47,18 +61,28 @@ export async function loader({ request }: Route.LoaderArgs) { } return { + isEmailPasswordSigninEnabled, isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo, + shouldAutoRedirectToOIDC, }; } export default function SignIn({ loaderData }: Route.ComponentProps) { - const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, isSignupEnabled, oidcProviderLabel, returnTo } = - loaderData; + const { + isEmailPasswordSigninEnabled, + isGoogleSSOEnabled, + isMicrosoftSSOEnabled, + isOIDCSSOEnabled, + isSignupEnabled, + oidcProviderLabel, + returnTo, + shouldAutoRedirectToOIDC, + } = loaderData; const { _ } = useLingui(); @@ -76,6 +100,27 @@ export default function SignIn({ loaderData }: Route.ComponentProps) { setIsEmbeddedRedirect(params.get('embedded') === 'true'); }, []); + useEffect(() => { + if (!shouldAutoRedirectToOIDC) { + return; + } + + void authClient.oidc.signIn({ redirectPath: returnTo ?? '/' }); + }, [shouldAutoRedirectToOIDC, returnTo]); + + if (shouldAutoRedirectToOIDC) { + return ( +
+
+ +

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

+
+
+ ); + } + return (
@@ -95,6 +140,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
() .post('/authorize', sValidator('json', ZSignInSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { email, password, totpCode, backupCode, csrfToken, captchaToken } = c.req.valid('json'); const loginLimitResult = await loginRateLimit.check({ @@ -244,6 +251,12 @@ export const emailPasswordRoute = new Hono() const { password, currentPassword } = c.req.valid('json'); const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { session, user } = await getSession(c); await updatePassword({ @@ -346,6 +359,12 @@ export const emailPasswordRoute = new Hono() .post('/forgot-password', sValidator('json', ZForgotPasswordSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { email } = c.req.valid('json'); const forgotLimitResult = await forgotPasswordRateLimit.check({ @@ -377,6 +396,12 @@ export const emailPasswordRoute = new Hono() .post('/reset-password', sValidator('json', ZResetPasswordSchema), async (c) => { const requestMetadata = c.get('requestMetadata'); + if (!isSigninEnabledForProvider('email')) { + throw new AppError(AuthenticationErrorCode.SigninDisabled, { + statusCode: 400, + }); + } + const { token, password } = c.req.valid('json'); const resetLimitResult = await resetPasswordRateLimit.check({ diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index 4768540d4..c4c8727ed 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -41,6 +41,14 @@ export const IS_OIDC_SSO_ENABLED = Boolean( export const OIDC_PROVIDER_LABEL = env('NEXT_PRIVATE_OIDC_PROVIDER_LABEL'); +/** + * Opt-out flag for the automatic OIDC redirect. + * + * When OIDC is the only enabled signin transport we redirect to the provider + * automatically. Set this to "true" to keep rendering the signin page instead. + */ +export const IS_OIDC_AUTO_REDIRECT_DISABLED = env('NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT') === 'true'; + export const USER_SECURITY_AUDIT_LOG_MAP: Record = { ACCOUNT_SSO_LINK: 'Linked account to SSO', ACCOUNT_SSO_UNLINK: 'Unlinked account from SSO', @@ -188,3 +196,22 @@ export const isSignupEnabledForProvider = (provider: 'email' | 'google' | 'micro return env(flagMap[provider]) !== 'true'; }; + +/** + * Check if signin is enabled for the given provider. + * The master switch takes precedence over the per-provider flags. + */ +export const isSigninEnabledForProvider = (provider: 'email' | 'google' | 'microsoft' | 'oidc'): boolean => { + if (env('NEXT_PUBLIC_DISABLE_SIGNIN') === 'true') { + return false; + } + + const flagMap = { + email: 'NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN', + google: 'NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN', + microsoft: 'NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN', + oidc: 'NEXT_PUBLIC_DISABLE_OIDC_SIGNIN', + } as const; + + return env(flagMap[provider]) !== 'true'; +}; diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index f1f748764..6ec6e94f1 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -8917,6 +8917,11 @@ msgstr "Weiterleitungs-URL" msgid "Redirecting" msgstr "Weiterleitung" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 454a46e1e..07cf034bc 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -8908,6 +8908,11 @@ msgstr "Redirect URL" msgid "Redirecting" msgstr "Redirecting" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "Redirecting to {0}..." + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 4eef75f10..2d44ea007 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -8917,6 +8917,11 @@ msgstr "URL de redirección" msgid "Redirecting" msgstr "Redireccionando" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index 181b21a64..fb2b175d2 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -8917,6 +8917,11 @@ msgstr "URL de redirection" msgid "Redirecting" msgstr "Redirection" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/it/web.po b/packages/lib/translations/it/web.po index d59511b72..1fd209ef4 100644 --- a/packages/lib/translations/it/web.po +++ b/packages/lib/translations/it/web.po @@ -8917,6 +8917,11 @@ msgstr "URL di reindirizzamento" msgid "Redirecting" msgstr "Reindirizzamento" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/ja/web.po b/packages/lib/translations/ja/web.po index 2c710627f..f2fc95818 100644 --- a/packages/lib/translations/ja/web.po +++ b/packages/lib/translations/ja/web.po @@ -8917,6 +8917,11 @@ msgstr "リダイレクト URL" msgid "Redirecting" msgstr "リダイレクト中" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/ko/web.po b/packages/lib/translations/ko/web.po index 802fe31f9..743a6ef9a 100644 --- a/packages/lib/translations/ko/web.po +++ b/packages/lib/translations/ko/web.po @@ -8917,6 +8917,11 @@ msgstr "리디렉션 URL" msgid "Redirecting" msgstr "리디렉션 중" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/nl/web.po b/packages/lib/translations/nl/web.po index 49953db32..4c8da7804 100644 --- a/packages/lib/translations/nl/web.po +++ b/packages/lib/translations/nl/web.po @@ -8917,6 +8917,11 @@ msgstr "Redirect-URL" msgid "Redirecting" msgstr "Doorsturen" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index 010ea2157..de84f4a84 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -8918,6 +8918,11 @@ msgstr "Adres URL przekierowania" msgid "Redirecting" msgstr "Przekierowywanie" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/pt-BR/web.po b/packages/lib/translations/pt-BR/web.po index 85490df57..027ae4255 100644 --- a/packages/lib/translations/pt-BR/web.po +++ b/packages/lib/translations/pt-BR/web.po @@ -8908,6 +8908,11 @@ msgstr "URL de Redirecionamento" msgid "Redirecting" msgstr "Redirecionando" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/lib/translations/zh/web.po b/packages/lib/translations/zh/web.po index e107bc848..975a3583c 100644 --- a/packages/lib/translations/zh/web.po +++ b/packages/lib/translations/zh/web.po @@ -8917,6 +8917,11 @@ msgstr "重定向 URL" msgid "Redirecting" msgstr "正在重定向" +#. placeholder {0}: oidcProviderLabel || 'OIDC' +#: apps/remix/app/routes/_unauthenticated+/signin.tsx +msgid "Redirecting to {0}..." +msgstr "" + #: apps/remix/app/components/forms/signup.tsx #: apps/remix/app/components/general/claim-account.tsx msgid "Registration Successful" diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 6757ed47c..d02df2be0 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -93,6 +93,13 @@ declare namespace NodeJS { NEXT_PUBLIC_DISABLE_OIDC_SIGNUP?: string; NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS?: string; + NEXT_PUBLIC_DISABLE_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_OIDC_SIGNIN?: string; + NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT?: string; + NEXT_PRIVATE_BROWSERLESS_URL?: string; NEXT_PRIVATE_JOBS_PROVIDER?: 'inngest' | 'local' | 'bullmq'; diff --git a/render.yaml b/render.yaml index 9a29fae4d..2b82e7f14 100644 --- a/render.yaml +++ b/render.yaml @@ -163,6 +163,18 @@ services: sync: false - key: NEXT_PUBLIC_DISABLE_OIDC_SIGNUP sync: false + - key: NEXT_PUBLIC_DISABLE_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_OIDC_SIGNIN + sync: false + - key: NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT + sync: false - key: NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS sync: false diff --git a/turbo.json b/turbo.json index c5c0db0f7..b417941fa 100644 --- a/turbo.json +++ b/turbo.json @@ -53,6 +53,12 @@ "NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP", "NEXT_PUBLIC_DISABLE_OIDC_SIGNUP", "NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS", + "NEXT_PUBLIC_DISABLE_SIGNIN", + "NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN", + "NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN", + "NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN", + "NEXT_PUBLIC_DISABLE_OIDC_SIGNIN", + "NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT", "NEXT_PRIVATE_PLAIN_API_KEY", "NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT", "NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY", From 5a8335e0eb01823b560cb5644bd1db2b105195d7 Mon Sep 17 00:00:00 2001 From: Arun Kumar <147901612+Arunkoo@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:49:21 +0530 Subject: [PATCH 18/41] fix: webhook payload contains stale deletedAt on document cancellation (#2980) --- packages/lib/server-only/document/delete-document.ts | 6 ++++-- packages/lib/server-only/envelope/create-envelope.ts | 2 +- packages/lib/server-only/envelope/duplicate-envelope.ts | 2 +- .../server-only/template/create-document-from-template.ts | 2 +- packages/lib/types/subscription.ts | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index 0a4456d5c..8dd0b69de 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -83,15 +83,17 @@ export const deleteDocument = async ({ id, userId, teamId, requestMetadata }: De // Handle hard or soft deleting the actual document if user has permission. if (hasDeleteAccess) { - await handleDocumentOwnerDelete({ + const updatedEnvelope = await handleDocumentOwnerDelete({ envelope, user, requestMetadata, }); + const envelopeForWebhook = { ...envelope, ...(updatedEnvelope ?? {}) }; + await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_CANCELLED, - data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeForWebhook)), userId, teamId, }); diff --git a/packages/lib/server-only/envelope/create-envelope.ts b/packages/lib/server-only/envelope/create-envelope.ts index 4ca73a387..2bbb079fd 100644 --- a/packages/lib/server-only/envelope/create-envelope.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -37,9 +37,9 @@ import { extractDerivedDocumentMeta } from '../../utils/document'; import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth'; import { buildTeamWhereQuery } from '../../utils/teams'; import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id'; +import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role'; import { resolveSignatureLevel } from '../signature-level/resolve-signature-level'; -import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; import { getTeamSettings } from '../team/get-team-settings'; import { assertUserNotDisabledById } from '../user/assert-user-not-disabled'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; diff --git a/packages/lib/server-only/envelope/duplicate-envelope.ts b/packages/lib/server-only/envelope/duplicate-envelope.ts index a904bb3f2..4770814f8 100644 --- a/packages/lib/server-only/envelope/duplicate-envelope.ts +++ b/packages/lib/server-only/envelope/duplicate-envelope.ts @@ -10,8 +10,8 @@ import { nanoid, prefixedId } from '../../universal/id'; import type { EnvelopeIdOptions } from '../../utils/envelope'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id'; -import { resolveSignatureLevel } from '../signature-level/resolve-signature-level'; import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; +import { resolveSignatureLevel } from '../signature-level/resolve-signature-level'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export interface DuplicateEnvelopeOptions { diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 114a3c2ef..47158c3f0 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -51,8 +51,8 @@ import { buildTeamWhereQuery } from '../../utils/teams'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { incrementDocumentId } from '../envelope/increment-id'; import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; -import { resolveSignatureLevel } from '../signature-level/resolve-signature-level'; import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits'; +import { resolveSignatureLevel } from '../signature-level/resolve-signature-level'; import { getTeamSettings } from '../team/get-team-settings'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { getOrganisationTemplateWhereInput } from './get-organisation-template-by-id'; diff --git a/packages/lib/types/subscription.ts b/packages/lib/types/subscription.ts index d25028e10..ba14433fc 100644 --- a/packages/lib/types/subscription.ts +++ b/packages/lib/types/subscription.ts @@ -52,7 +52,7 @@ export const ZClaimFlagsSchema = z.object({ signingReminders: z.boolean().optional(), cscQesSigning: z.boolean().optional(), - + /** * Controls whether an organisation is prevented from sending emails. * From 393b51d4847bb0ff3b1c6f47341aa9c76770eef4 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 2 Jul 2026 14:52:28 +1000 Subject: [PATCH 19/41] fix: add sticky form update button (#3056) --- .../forms/branding-preferences-form.tsx | 100 +++++++++++---- .../forms/document-preferences-form.tsx | 26 ++-- .../forms/email-preferences-form.tsx | 27 ++-- .../components/forms/form-sticky-save-bar.tsx | 119 ++++++++++++++++++ .../forms/organisation-update-form.tsx | 39 ++---- .../app/components/forms/team-update-form.tsx | 39 ++---- .../o.$orgUrl.settings.branding.tsx | 3 + .../o.$orgUrl.settings.document.tsx | 2 + .../o.$orgUrl.settings.email.tsx | 2 + .../t.$teamUrl+/settings.branding.tsx | 3 + .../t.$teamUrl+/settings.document.tsx | 2 + .../t.$teamUrl+/settings.email.tsx | 2 + .../update-organisation-member-role.spec.ts | 2 +- .../envelope-expiration-settings.spec.ts | 12 +- .../include-document-certificate.spec.ts | 10 +- .../organisations/manage-organisation.spec.ts | 2 +- .../organisation-team-preferences.spec.ts | 16 +-- .../app-tests/e2e/teams/manage-team.spec.ts | 2 +- .../e2e/teams/team-settings-save-bar.spec.ts | 79 ++++++++++++ .../e2e/teams/team-signature-settings.spec.ts | 4 +- 20 files changed, 359 insertions(+), 132 deletions(-) create mode 100644 apps/remix/app/components/forms/form-sticky-save-bar.tsx create mode 100644 packages/app-tests/e2e/teams/team-settings-save-bar.spec.ts diff --git a/apps/remix/app/components/forms/branding-preferences-form.tsx b/apps/remix/app/components/forms/branding-preferences-form.tsx index e420fdae7..561fb5512 100644 --- a/apps/remix/app/components/forms/branding-preferences-form.tsx +++ b/apps/remix/app/components/forms/branding-preferences-form.tsx @@ -21,6 +21,8 @@ import { z } from 'zod'; import { useOptionalCurrentTeam } from '~/providers/team'; import { useCspNonce } from '~/utils/nonce'; +import { FormStickySaveBar } from './form-sticky-save-bar'; + const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; @@ -71,38 +73,82 @@ export function BrandingPreferencesForm({ const parsedColors = ZCssVarsSchema.safeParse(settings.brandingColors); const initialColors = parsedColors.success ? parsedColors.data : {}; + // The saved state the form maps to. Used both as the reactive `values` source and as + // the explicit target for a Reset (see handleReset). + const savedValues: TBrandingPreferencesFormSchema = { + brandingEnabled: settings.brandingEnabled ?? null, + brandingUrl: settings.brandingUrl ?? '', + brandingLogo: undefined, + brandingCompanyDetails: settings.brandingCompanyDetails ?? '', + brandingColors: initialColors, + brandingCss: settings.brandingCss ?? '', + }; + const form = useForm({ - values: { - brandingEnabled: settings.brandingEnabled ?? null, - brandingUrl: settings.brandingUrl ?? '', - brandingLogo: undefined, - brandingCompanyDetails: settings.brandingCompanyDetails ?? '', - brandingColors: initialColors, - brandingCss: settings.brandingCss ?? '', - }, + values: savedValues, resolver: zodResolver(ZBrandingPreferencesFormSchema), }); const isBrandingEnabled = form.watch('brandingEnabled'); + const getSavedLogoPreviewUrl = () => { + if (!settings.brandingLogo) { + return ''; + } + + const file = JSON.parse(settings.brandingLogo); + + if (!('type' in file) || !('data' in file)) { + return ''; + } + + const logoUrl = + context === 'Team' + ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${team?.id}` + : `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/organisation/${organisation?.id}`; + + return `${logoUrl}?v=${Date.now()}`; + }; + useEffect(() => { - if (settings.brandingLogo) { - const file = JSON.parse(settings.brandingLogo); + const savedLogoPreviewUrl = getSavedLogoPreviewUrl(); - if ('type' in file && 'data' in file) { - const logoUrl = - context === 'Team' - ? `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/team/${team?.id}` - : `${NEXT_PUBLIC_WEBAPP_URL()}/api/branding/logo/organisation/${organisation?.id}`; - - setPreviewUrl(logoUrl + '?v=' + Date.now()); - setHasLoadedPreview(true); - } + if (savedLogoPreviewUrl) { + setPreviewUrl(savedLogoPreviewUrl); } setHasLoadedPreview(true); }, [settings.brandingLogo]); + // Reset the form to the saved values. The form is driven by the `values` prop (no + // `defaultValues`), so `reset()` with no argument doesn't re-baseline the dirty check; + // passing the saved values clears the per-field dirty tracking (dirtyFields). + const handleReset = () => { + setPreviewUrl(getSavedLogoPreviewUrl()); + form.reset(savedValues); + }; + + // `formState.isDirty` is unreliable for a `values`-driven form: after a reset (or a + // save + refetch) it can stay true even though every field already matches its saved + // value and `dirtyFields` is empty. Derive the flag from `dirtyFields` instead so the + // sticky save bar reliably disappears. + const hasUnsavedChanges = Object.keys(form.formState.dirtyFields).length > 0; + + // Re-baseline the form to the just-saved state after a successful submit. The `values` + // prop re-syncs most fields once the route refetches, but write-only fields (the logo + // is a File that isn't reflected back into `values`) would otherwise stay dirty and + // keep the save bar visible. Relies on the page handler rethrowing on error so we only + // re-baseline on success. + const handleFormSubmit = form.handleSubmit(async (data) => { + try { + await onFormSubmit(data); + } catch { + return; + } + + form.reset(form.getValues()); + }); + // Cleanup ObjectURL on unmount or when previewUrl changes useEffect(() => { return () => { @@ -114,7 +160,7 @@ export function BrandingPreferencesForm({ return ( - +
- {!isBrandingEnabled &&
} + {!isBrandingEnabled &&
} - {!isBrandingEnabled &&
} + {!isBrandingEnabled &&
}
@@ -538,11 +584,11 @@ export function BrandingPreferencesForm({
)} -
- -
+
diff --git a/apps/remix/app/components/forms/document-preferences-form.tsx b/apps/remix/app/components/forms/document-preferences-form.tsx index ee552d043..f1a0b6d8a 100644 --- a/apps/remix/app/components/forms/document-preferences-form.tsx +++ b/apps/remix/app/components/forms/document-preferences-form.tsx @@ -21,7 +21,6 @@ import { ReminderSettingsPicker } from '@documenso/ui/components/document/remind import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select'; import { Alert } from '@documenso/ui/primitives/alert'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; -import { Button } from '@documenso/ui/primitives/button'; import { Combobox } from '@documenso/ui/primitives/combobox'; import { Form, @@ -46,6 +45,7 @@ import { z } from 'zod'; import { useOptionalCurrentTeam } from '~/providers/team'; import { DefaultRecipientsMultiSelectCombobox } from '../general/default-recipients-multiselect-combobox'; +import { FormStickySaveBar } from './form-sticky-save-bar'; /** * Can't infer this from the schema since we need to keep the schema inside the component to allow @@ -147,9 +147,21 @@ export const DocumentPreferencesForm = ({ resolver: zodResolver(ZDocumentPreferencesFormSchema), }); + const handleFormSubmit = form.handleSubmit(async (data) => { + try { + await onFormSubmit(data); + } catch { + // The page handler surfaces its own error toast. Keep the form dirty so + // the save bar stays visible and the user can retry. + return; + } + + form.reset(data); + }); + return (
- +
{!isPersonalLayoutMode && ( )} -
- -
+ form.reset()} + />
diff --git a/apps/remix/app/components/forms/email-preferences-form.tsx b/apps/remix/app/components/forms/email-preferences-form.tsx index 4ab981d93..818868005 100644 --- a/apps/remix/app/components/forms/email-preferences-form.tsx +++ b/apps/remix/app/components/forms/email-preferences-form.tsx @@ -4,7 +4,6 @@ import { DEFAULT_DOCUMENT_EMAIL_SETTINGS, ZDocumentEmailSettingsSchema } from '@ import { zEmail } from '@documenso/lib/utils/zod'; import { trpc } from '@documenso/trpc/react'; import { DocumentEmailCheckboxes } from '@documenso/ui/components/document/document-email-checkboxes'; -import { Button } from '@documenso/ui/primitives/button'; import { Form, FormControl, @@ -22,6 +21,8 @@ import type { TeamGlobalSettings } from '@prisma/client'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { FormStickySaveBar } from './form-sticky-save-bar'; + const ZEmailPreferencesFormSchema = z.object({ emailId: z.string().nullable(), emailReplyTo: zEmail().nullable(), @@ -59,9 +60,21 @@ export const EmailPreferencesForm = ({ settings, onFormSubmit, canInherit }: Ema const emails = emailData?.data || []; + const handleFormSubmit = form.handleSubmit(async (data) => { + try { + await onFormSubmit(data); + } catch { + // The page handler surfaces its own error toast. Keep the form dirty so + // the save bar stays visible and the user can retry. + return; + } + + form.reset(data); + }); + return (
- +
{organisation.organisationClaim.flags.emailDomains && ( -
- -
+ form.reset()} + />
diff --git a/apps/remix/app/components/forms/form-sticky-save-bar.tsx b/apps/remix/app/components/forms/form-sticky-save-bar.tsx new file mode 100644 index 000000000..1d37f9482 --- /dev/null +++ b/apps/remix/app/components/forms/form-sticky-save-bar.tsx @@ -0,0 +1,119 @@ +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { AnimatePresence, motion } from 'framer-motion'; +import { AlertTriangleIcon } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; + +export type FormStickySaveBarProps = { + isDirty: boolean; + isSubmitting: boolean; + onReset: () => void; +}; + +/** + * A single `position: sticky` bar rendered at the bottom of the form. + * + * - When the form's end is on screen it settles into place as a plain footer (just the + * Reset / Save buttons). + * - When the form's end is scrolled off, it sticks to the bottom of the viewport and + * shows the "unsaved changes" pill chrome. + * + * Because it's the same element in the form's flow, it auto-aligns to the form and the + * float <-> dock hand-off is a native, scroll-linked transition (no measurement, no + * shared-layout morph). A 1px sentinel below it detects the stuck state so we can toggle + * the pill chrome. + */ +export const FormStickySaveBar = ({ isDirty, isSubmitting, onReset }: FormStickySaveBarProps) => { + const { t } = useLingui(); + + const sentinelRef = useRef(null); + const [isStuck, setIsStuck] = useState(false); + + useEffect(() => { + const sentinel = sentinelRef.current; + + if (!sentinel) { + return; + } + + // The sentinel sits at the bar's resting position (the end of the form). While the + // bar is stuck to the bottom of the viewport the sentinel is scrolled past (out of + // view); once you reach the form's end it comes into view and the bar settles. + const observer = new IntersectionObserver( + ([entry]) => { + setIsStuck(!entry.isIntersecting); + }, + { + root: null, + rootMargin: '0px 0px -24px 0px', + threshold: 0, + }, + ); + + observer.observe(sentinel); + + return () => { + observer.disconnect(); + }; + }, []); + + // Show the floating pill chrome only when there are unsaved changes AND the form's + // end is off screen. + const isFloating = isDirty && isStuck; + + return ( + <> +
+ + {isFloating && ( + + + + You have unsaved changes + + + )} + + +
+ {isDirty && ( + + )} + + +
+
+ + {/* Sentinel: detects when the sticky bar is floating (stuck) vs settled (docked). */} +
+ + ); +}; diff --git a/apps/remix/app/components/forms/organisation-update-form.tsx b/apps/remix/app/components/forms/organisation-update-form.tsx index 7ac15b1c7..5d4c2c88f 100644 --- a/apps/remix/app/components/forms/organisation-update-form.tsx +++ b/apps/remix/app/components/forms/organisation-update-form.tsx @@ -4,7 +4,6 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { ZUpdateOrganisationRequestSchema } from '@documenso/trpc/server/organisation-router/update-organisation.types'; -import { Button } from '@documenso/ui/primitives/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -12,11 +11,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { AnimatePresence, motion } from 'framer-motion'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import type { z } from 'zod'; +import { FormStickySaveBar } from './form-sticky-save-bar'; + const ZOrganisationUpdateFormSchema = ZUpdateOrganisationRequestSchema.shape.data.pick({ name: true, url: true, @@ -137,36 +137,11 @@ export const OrganisationUpdateForm = () => { )} /> -
- - {form.formState.isDirty && ( - - - - )} - - - -
+ form.reset()} + />
diff --git a/apps/remix/app/components/forms/team-update-form.tsx b/apps/remix/app/components/forms/team-update-form.tsx index fdf569f95..8dddaee2b 100644 --- a/apps/remix/app/components/forms/team-update-form.tsx +++ b/apps/remix/app/components/forms/team-update-form.tsx @@ -2,7 +2,6 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { ZUpdateTeamRequestSchema } from '@documenso/trpc/server/team-router/update-team.types'; -import { Button } from '@documenso/ui/primitives/button'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -10,11 +9,12 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { AnimatePresence, motion } from 'framer-motion'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import type { z } from 'zod'; +import { FormStickySaveBar } from './form-sticky-save-bar'; + export type UpdateTeamDialogProps = { teamId: number; teamName: string; @@ -135,36 +135,11 @@ export const TeamUpdateForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogPr )} /> -
- - {form.formState.isDirty && ( - - - - )} - - - -
+ form.reset()} + /> diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx index 9243b9d4c..707c4ad96 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx @@ -104,6 +104,9 @@ export default function OrganisationSettingsBrandingPage() { description: t`We were unable to update your branding preferences at this time, please try again later`, variant: 'destructive', }); + + // Rethrow so the form knows the save failed and keeps the unsaved changes. + throw err; } }; diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.document.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.document.tsx index de9786aed..9d73e00bc 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.document.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.document.tsx @@ -105,6 +105,8 @@ export default function OrganisationSettingsDocumentPage() { description: t`We were unable to update your document preferences at this time, please try again later`, variant: 'destructive', }); + + throw err; } }; diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx index 16144b1a5..0db136f7b 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx @@ -49,6 +49,8 @@ export default function OrganisationSettingsGeneral() { description: t`We were unable to update your email preferences at this time, please try again later`, variant: 'destructive', }); + + throw err; } }; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx index b674dc460..0401d4da2 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx @@ -99,6 +99,9 @@ export default function TeamsSettingsPage() { description: t`We were unable to update your branding preferences at this time, please try again later`, variant: 'destructive', }); + + // Rethrow so the form knows the save failed and keeps the unsaved changes. + throw err; } }; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx index b42f8a66e..fd734847f 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx @@ -96,6 +96,8 @@ export default function TeamsSettingsPage() { description: t`We were unable to update your document preferences at this time, please try again later`, variant: 'destructive', }); + + throw err; } }; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx index e624c57e2..8b64a21bc 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx @@ -49,6 +49,8 @@ export default function TeamEmailSettingsGeneral() { description: t`We were unable to update your email preferences at this time, please try again later`, variant: 'destructive', }); + + throw err; } }; diff --git a/packages/app-tests/e2e/admin/organisations/update-organisation-member-role.spec.ts b/packages/app-tests/e2e/admin/organisations/update-organisation-member-role.spec.ts index 2e0d53fb5..b73ed6f7f 100644 --- a/packages/app-tests/e2e/admin/organisations/update-organisation-member-role.spec.ts +++ b/packages/app-tests/e2e/admin/organisations/update-organisation-member-role.spec.ts @@ -526,7 +526,7 @@ test('[ADMIN]: verify organisation access after ownership change', async ({ page // Should be able to access organisation settings await expect(page.getByText('Organisation Settings')).toBeVisible(); await expect(page.getByLabel('Organisation Name*')).toBeVisible(); - await expect(page.getByRole('button', { name: 'Update organisation' })).toBeVisible(); + await expect(page.getByLabel('Organisation Name*')).toBeEnabled(); // Should have delete permissions await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); diff --git a/packages/app-tests/e2e/envelopes/envelope-expiration-settings.spec.ts b/packages/app-tests/e2e/envelopes/envelope-expiration-settings.spec.ts index 806585193..fa1545d17 100644 --- a/packages/app-tests/e2e/envelopes/envelope-expiration-settings.spec.ts +++ b/packages/app-tests/e2e/envelopes/envelope-expiration-settings.spec.ts @@ -19,7 +19,7 @@ test('[ENVELOPE_EXPIRATION]: set custom expiration period at organisation level' }); // Wait for the form to load. - await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible(); + await expect(page.getByTestId('document-language-trigger')).toBeVisible(); // Change the amount to 2. const amountInput = page.getByTestId('envelope-expiration-amount'); @@ -35,7 +35,7 @@ test('[ENVELOPE_EXPIRATION]: set custom expiration period at organisation level' await unitTrigger.click(); await page.getByRole('option', { name: 'Weeks' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); // Verify via database. @@ -57,14 +57,14 @@ test('[ENVELOPE_EXPIRATION]: disable expiration at organisation level', async ({ redirectPath: `/o/${organisation.url}/settings/document`, }); - await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible(); + await expect(page.getByTestId('document-language-trigger')).toBeVisible(); // Find the mode select (shows "Custom duration") and change to "Never expires". const modeTrigger = page.getByTestId('envelope-expiration-mode'); await modeTrigger.click(); await page.getByRole('option', { name: 'Never expires' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); // Verify via database. @@ -109,7 +109,7 @@ test('[ENVELOPE_EXPIRATION]: team overrides organisation expiration', async ({ p redirectPath: `/t/${team.url}/settings/document`, }); - await expect(page.getByRole('button', { name: 'Update' }).first()).toBeVisible(); + await expect(page.getByTestId('document-language-trigger')).toBeVisible(); // The expiration picker mode select should show "Inherit from organisation" by default. const modeTrigger = page.getByTestId('envelope-expiration-mode'); @@ -128,7 +128,7 @@ test('[ENVELOPE_EXPIRATION]: team overrides organisation expiration', async ({ p await unitTrigger.click(); await page.getByRole('option', { name: 'Days' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); // Verify team setting is overridden. diff --git a/packages/app-tests/e2e/features/include-document-certificate.spec.ts b/packages/app-tests/e2e/features/include-document-certificate.spec.ts index 335fb2389..6d44581f6 100644 --- a/packages/app-tests/e2e/features/include-document-certificate.spec.ts +++ b/packages/app-tests/e2e/features/include-document-certificate.spec.ts @@ -324,10 +324,7 @@ test.describe('Signing Certificate Tests', () => { .click(); await page.getByRole('option', { name: 'No' }).click(); - await page - .getByRole('button', { name: /Update/ }) - .first() - .click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await page.waitForTimeout(1000); @@ -347,10 +344,7 @@ test.describe('Signing Certificate Tests', () => { .getByRole('combobox') .click(); await page.getByRole('option', { name: 'Yes' }).click(); - await page - .getByRole('button', { name: /Update/ }) - .first() - .click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await page.waitForTimeout(1000); diff --git a/packages/app-tests/e2e/organisations/manage-organisation.spec.ts b/packages/app-tests/e2e/organisations/manage-organisation.spec.ts index f14777b08..23fcdedc6 100644 --- a/packages/app-tests/e2e/organisations/manage-organisation.spec.ts +++ b/packages/app-tests/e2e/organisations/manage-organisation.spec.ts @@ -60,7 +60,7 @@ test('[ORGANISATIONS]: manage general settings', async ({ page }) => { await page.getByLabel('Organisation URL*').clear(); await page.getByLabel('Organisation URL*').fill(updatedOrganisationId); - await page.getByRole('button', { name: 'Update organisation' }).click(); + await page.getByRole('button', { name: 'Save changes' }).click(); // Check we have been redirected to the new organisation URL and the name is updated. await page.waitForURL(`/o/${updatedOrganisationId}/settings/general`); diff --git a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts index 48ab68685..336222d93 100644 --- a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts +++ b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts @@ -39,7 +39,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { await page.getByRole('option', { name: 'No' }).click(); await page.getByTestId('include-signing-certificate-trigger').click(); await page.getByRole('option', { name: 'No' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); const teamSettings = await getTeamSettings({ @@ -73,7 +73,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { await page.getByTestId('document-date-format-trigger').click(); await page.getByRole('option', { name: 'MM/DD/YYYY', exact: true }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible(); const updatedTeamSettings = await getTeamSettings({ @@ -128,7 +128,7 @@ test('[ORGANISATIONS]: manage branding preferences', async ({ page }) => { await page.getByRole('textbox', { name: 'Brand Website' }).fill('https://documenso.com'); await page.getByRole('textbox', { name: 'Brand Details' }).click(); await page.getByRole('textbox', { name: 'Brand Details' }).fill('BrandDetails'); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible(); const teamSettings = await getTeamSettings({ @@ -150,7 +150,7 @@ test('[ORGANISATIONS]: manage branding preferences', async ({ page }) => { await page.getByRole('textbox', { name: 'Brand Website' }).fill('https://example.com'); await page.getByRole('textbox', { name: 'Brand Details' }).click(); await page.getByRole('textbox', { name: 'Brand Details' }).fill('UpdatedBrandDetails'); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible(); const updatedTeamSettings = await getTeamSettings({ @@ -165,7 +165,7 @@ test('[ORGANISATIONS]: manage branding preferences', async ({ page }) => { // Test inheritance by setting team back to inherit from organisation await page.getByTestId('enable-branding').click(); await page.getByRole('option', { name: 'Inherit from organisation' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible(); await page.waitForTimeout(2000); @@ -208,7 +208,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { await page.getByRole('checkbox', { name: 'Email the signer if the document is still pending' }).uncheck(); await page.getByRole('checkbox', { name: 'Email recipients when a pending document is deleted' }).uncheck(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible(); const teamSettings = await getTeamSettings({ @@ -245,7 +245,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { await page.getByRole('checkbox', { name: 'Email recipients when the document is completed', exact: true }).uncheck(); await page.getByRole('checkbox', { name: 'Email the owner when the document is completed' }).uncheck(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible(); const updatedTeamSettings = await getTeamSettings({ @@ -292,7 +292,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { await page.getByRole('textbox', { name: 'Reply to email' }).fill(''); await page.getByRole('combobox').filter({ hasText: 'Override organisation settings' }).click(); await page.getByRole('option', { name: 'Inherit from organisation' }).click(); - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible(); await page.waitForTimeout(1000); diff --git a/packages/app-tests/e2e/teams/manage-team.spec.ts b/packages/app-tests/e2e/teams/manage-team.spec.ts index 293ac0113..5e4892444 100644 --- a/packages/app-tests/e2e/teams/manage-team.spec.ts +++ b/packages/app-tests/e2e/teams/manage-team.spec.ts @@ -66,7 +66,7 @@ test('[TEAMS]: update team', async ({ page }) => { await page.getByLabel('Team URL*').clear(); await page.getByLabel('Team URL*').fill(updatedTeamId); - await page.getByRole('button', { name: 'Update team' }).click(); + await page.getByRole('button', { name: 'Save changes' }).click(); // Check we have been redirected to the new team URL and the name is updated. await page.waitForURL(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${updatedTeamId}/settings`); diff --git a/packages/app-tests/e2e/teams/team-settings-save-bar.spec.ts b/packages/app-tests/e2e/teams/team-settings-save-bar.spec.ts new file mode 100644 index 000000000..272b649e7 --- /dev/null +++ b/packages/app-tests/e2e/teams/team-settings-save-bar.spec.ts @@ -0,0 +1,79 @@ +import { seedUser } from '@documenso/prisma/seed/users'; +import { expect, test } from '@playwright/test'; + +import { apiSignin } from '../fixtures/authentication'; + +test('[TEAMS]: settings save bar docks at the bottom of the form', async ({ page }) => { + const { user, team } = await seedUser(); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/t/${team.url}/settings`, + }); + + await expect(page.getByLabel('Team Name*')).toBeVisible(); + + const saveButton = page.getByRole('button', { name: 'Save changes' }); + + // Pristine: the docked Save button is present but disabled; no Undo, no floating notice. + await expect(saveButton).toBeVisible(); + await expect(saveButton).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Undo' })).toHaveCount(0); + await expect(page.getByText('You have unsaved changes')).not.toBeVisible(); + + // Make a change → Save enables and Undo appears. + const updatedName = `team-${Date.now()}`; + await page.getByLabel('Team Name*').clear(); + await page.getByLabel('Team Name*').fill(updatedName); + + await expect(saveButton).toBeEnabled(); + await expect(page.getByRole('button', { name: 'Undo' })).toBeVisible(); + + // Undo → value restored, Save disabled again, Undo gone. + await page.getByRole('button', { name: 'Undo' }).click(); + await expect(page.getByLabel('Team Name*')).toHaveValue(team.name); + await expect(saveButton).toBeDisabled(); + await expect(page.getByRole('button', { name: 'Undo' })).toHaveCount(0); + + // Change again → Save → success toast, returns to a pristine (disabled) state. + await page.getByLabel('Team Name*').clear(); + await page.getByLabel('Team Name*').fill(updatedName); + await expect(saveButton).toBeEnabled(); + await saveButton.click(); + + await expect(page.getByText('Your team has been successfully updated.').first()).toBeVisible(); + await expect(saveButton).toBeDisabled(); +}); + +test('[ORGANISATIONS]: settings save bar floats when the form footer is off-screen', async ({ page }) => { + const { user, organisation } = await seedUser({ + isPersonalOrganisation: false, + }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/o/${organisation.url}/settings/document`, + }); + + // Wait for the long document-preferences form to load. + await expect(page.getByTestId('document-language-trigger')).toBeVisible(); + + // Pristine: no floating notice even though the footer is below the fold. + await expect(page.getByText('You have unsaved changes')).not.toBeVisible(); + + // Edit a field near the top → the footer is off-screen, so the floating pill appears. + await page.getByTestId('document-language-trigger').click(); + await page.getByRole('option', { name: 'German' }).click(); + + await expect(page.getByText('You have unsaved changes')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save changes' })).toBeVisible(); + + // Scroll to the footer → the floating pill merges into the docked buttons and the + // notice disappears. + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + await expect(page.getByText('You have unsaved changes')).not.toBeVisible(); + await expect(page.getByRole('button', { name: 'Save changes' })).toBeVisible(); +}); diff --git a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts index 7b4e4541e..078d0cab7 100644 --- a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts +++ b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts @@ -75,7 +75,7 @@ test('[TEAMS]: check signature modes can be disabled', async ({ page }) => { await item.click(); } - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); // Wait for the update to complete await expect(page.getByText('Document preferences updated', { exact: true })).toBeVisible(); @@ -140,7 +140,7 @@ test('[TEAMS]: check signature modes work for templates', async ({ page }) => { await item.click(); } - await page.getByRole('button', { name: 'Update' }).first().click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); // Wait for finish await expect(page.getByText('Document preferences updated', { exact: true })).toBeVisible(); From 2332b0316be550548964d0af6452b9a3fa3c7be5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:58:56 +1000 Subject: [PATCH 20/41] chore: extract translations (#3013) --- packages/lib/translations/de/web.po | 44 ++++++++++-------- packages/lib/translations/en/web.po | 64 +++++++++++++++++++------- packages/lib/translations/es/web.po | 44 ++++++++++-------- packages/lib/translations/fr/web.po | 44 ++++++++++-------- packages/lib/translations/it/web.po | 44 ++++++++++-------- packages/lib/translations/ja/web.po | 44 ++++++++++-------- packages/lib/translations/ko/web.po | 44 ++++++++++-------- packages/lib/translations/nl/web.po | 44 ++++++++++-------- packages/lib/translations/pl/web.po | 45 ++++++++++-------- packages/lib/translations/pt-BR/web.po | 64 +++++++++++++++++++------- packages/lib/translations/zh/web.po | 44 ++++++++++-------- 11 files changed, 328 insertions(+), 197 deletions(-) diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index 6ec6e94f1..596094b11 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -2441,6 +2441,7 @@ msgstr "Branding-Logo" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Markenpräferenzen" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Derzeit können alle Organisationsmitglieder auf dieses Team zugreifen" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Zurzeit kann das Branding nur für Teams und darüber konfiguriert werden." @@ -4214,8 +4216,8 @@ msgstr "Dokument storniert" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Dokument storniert" @@ -7942,6 +7944,11 @@ msgstr "Original" msgid "Otherwise, the document will be created as a draft." msgstr "Andernfalls wird das Dokument als Entwurf erstellt." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "Umschlag erneut senden" msgid "Resend verification" msgstr "Bestätigung erneut senden" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Zurücksetzen" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "Als Vorlage speichern" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Änderungen speichern" @@ -10224,6 +10230,11 @@ msgstr "Website Einstellungen" msgid "Skip" msgstr "Überspringen" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Einige Unterzeichner haben noch kein Unterschriftsfeld zugewiesen bekommen. Bitte weisen Sie jedem Unterzeichner mindestens ein Unterschriftsfeld zu, bevor Sie fortfahren." @@ -12254,6 +12265,7 @@ msgstr "Nicht autorisiert" msgid "Uncompleted" msgstr "Unvollendet" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Rückgängig" @@ -12303,6 +12315,10 @@ msgstr "Verknüpfung aufheben" msgid "Unpin" msgstr "Lösen" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "Unbetitelte Gruppe" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "Banner aktualisieren" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Rechnungsdaten aktualisieren" @@ -12374,10 +12388,6 @@ msgstr "E-Mail aktualisieren" msgid "Update Fields" msgstr "Felder aktualisieren" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Organisation aktualisieren" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "Rolle aktualisieren" msgid "Update Subscription Claim" msgstr "Abonnementsanspruch aktualisieren" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Team aktualisieren" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "Warten" msgid "Waiting for others" msgstr "Warten auf andere" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Warten auf andere, um die Unterzeichnung abzuschließen." @@ -13921,8 +13927,7 @@ msgstr "Du wurdest eingeladen, {0} auf Documenso beizutreten" msgid "You have been invited to join the following organisation" msgstr "Sie wurden eingeladen, der folgenden Organisation beizutreten" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Du wurdest von einem Dokument entfernt" @@ -14042,6 +14047,10 @@ msgstr "Sie haben den Zugriff erfolgreich widerrufen." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Sie haben das Recht, Ihre Zustimmung zur Verwendung elektronischer Unterschriften jederzeit vor Abschluss des Unterzeichnungsprozesses zu widerrufen. Um Ihre Zustimmung zu widerrufen, kontaktieren Sie bitte den Absender des Dokuments. Sollten Sie den Absender nicht erreichen, können Sie sich für Unterstützung an <0>{SUPPORT_EMAIL} wenden. Seien Sie sich bewusst, dass der Widerruf der Zustimmung den Abschluss der zugehörigen Transaktion oder Dienstleistung verzögern oder stoppen kann." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Sie haben {memberName} aktualisiert." @@ -14721,4 +14730,3 @@ msgstr "Ihr Verifizierungscode:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 07cf034bc..23d2f1c8f 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -2436,6 +2436,7 @@ msgstr "Branding Logo" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Branding Preferences" @@ -3567,6 +3568,7 @@ msgid "Currently all organisation members can access this team" msgstr "Currently all organisation members can access this team" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Currently branding can only be configured for Teams and above plans." @@ -4209,8 +4211,8 @@ msgstr "Document cancelled" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Document Cancelled" @@ -7937,6 +7939,11 @@ msgstr "Original" msgid "Otherwise, the document will be created as a draft." msgstr "Otherwise, the document will be created as a draft." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "Overlapping fields detected" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -8772,6 +8779,10 @@ msgstr "Recipient ID:" msgid "Recipient rejected the document" msgstr "Recipient rejected the document" +#: packages/lib/utils/document-audit-logs.ts +msgid "Recipient rejected the document externally" +msgstr "Recipient rejected the document externally" + #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Recipient removed" msgstr "Recipient removed" @@ -9188,9 +9199,7 @@ msgstr "Resend Envelope" msgid "Resend verification" msgstr "Resend verification" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Reset" @@ -9375,6 +9384,7 @@ msgid "Save as Template" msgstr "Save as Template" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Save changes" @@ -10215,6 +10225,11 @@ msgstr "Site Settings" msgid "Skip" msgstr "Skip" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." @@ -11093,6 +11108,23 @@ msgstr "The document signing process will be stopped" msgid "The document was created but could not be sent to recipients." msgstr "The document was created but could not be sent to recipients." +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally by {onBehalfOf} on behalf of {user}" +msgstr "The document was rejected externally by {onBehalfOf} on behalf of {user}" + +#: packages/lib/utils/document-audit-logs.ts +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally by {onBehalfOf} on behalf of the recipient" +msgstr "The document was rejected externally by {onBehalfOf} on behalf of the recipient" + +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally on behalf of {user}" +msgstr "The document was rejected externally on behalf of {user}" + +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally on behalf of the recipient" +msgstr "The document was rejected externally on behalf of the recipient" + #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "The document will be hidden from your account" msgstr "The document will be hidden from your account" @@ -12228,6 +12260,7 @@ msgstr "Unauthorized" msgid "Uncompleted" msgstr "Uncompleted" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Undo" @@ -12277,6 +12310,10 @@ msgstr "Unlink" msgid "Unpin" msgstr "Unpin" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "Unsaved changes" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12296,9 +12333,6 @@ msgstr "Untitled Group" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12321,6 +12355,7 @@ msgstr "Update Banner" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Update Billing" @@ -12348,10 +12383,6 @@ msgstr "Update email" msgid "Update Fields" msgstr "Update Fields" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Update organisation" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12387,10 +12418,6 @@ msgstr "Update role" msgid "Update Subscription Claim" msgstr "Update Subscription Claim" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Update team" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12921,7 +12948,7 @@ msgstr "Waiting" msgid "Waiting for others" msgstr "Waiting for others" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Waiting for others to complete signing." @@ -13895,8 +13922,7 @@ msgstr "You have been invited to join {0} on Documenso" msgid "You have been invited to join the following organisation" msgstr "You have been invited to join the following organisation" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "You have been removed from a document" @@ -14016,6 +14042,10 @@ msgstr "You have successfully revoked access." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "You have unsaved changes" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "You have updated {memberName}." diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 2d44ea007..eaa06c120 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -2441,6 +2441,7 @@ msgstr "Logotipo de Marca" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Preferencias de marca" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Actualmente, todos los miembros de la organización pueden acceder a este equipo" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Actualmente la marca solo se puede configurar para Equipos y planes superiores." @@ -4214,8 +4216,8 @@ msgstr "Documento cancelado" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Documento cancelado" @@ -7942,6 +7944,11 @@ msgstr "Original" msgid "Otherwise, the document will be created as a draft." msgstr "De lo contrario, el documento se creará como un borrador." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "Reenviar sobre (envelope)" msgid "Resend verification" msgstr "Reenviar verificación" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Restablecer" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "Guardar como plantilla" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Guardar cambios" @@ -10224,6 +10230,11 @@ msgstr "Configuraciones del sitio" msgid "Skip" msgstr "Omitir" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Algunos firmantes no han sido asignados a un campo de firma. Asigne al menos 1 campo de firma a cada firmante antes de continuar." @@ -12254,6 +12265,7 @@ msgstr "No autorizado" msgid "Uncompleted" msgstr "Incompleto" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Deshacer" @@ -12303,6 +12315,10 @@ msgstr "Desvincular" msgid "Unpin" msgstr "Desanclar" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "Grupo sin título" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "Actualizar banner" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Actualizar facturación" @@ -12374,10 +12388,6 @@ msgstr "Actualizar correo electrónico" msgid "Update Fields" msgstr "Actualizar Campos" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Actualizar organización" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "Actualizar rol" msgid "Update Subscription Claim" msgstr "Actualizar reclamo de suscripción" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Actualizar equipo" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "Esperando" msgid "Waiting for others" msgstr "Esperando a otros" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Esperando a que otros completen la firma." @@ -13921,8 +13927,7 @@ msgstr "Te han invitado a unirte a {0} en Documenso" msgid "You have been invited to join the following organisation" msgstr "Has sido invitado a unirte a la siguiente organización" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Te han eliminado de un documento" @@ -14042,6 +14047,10 @@ msgstr "Has revocado el acceso con éxito." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Usted tiene el derecho de retirar su consentimiento para usar firmas electrónicas en cualquier momento antes de completar el proceso de firma. Para retirar su consentimiento, comuníquese con el remitente del documento. Si no se comunica con el remitente, puede comunicarse con <0>{SUPPORT_EMAIL} para obtener asistencia. Tenga en cuenta que retirar el consentimiento puede retrasar o detener la finalización de la transacción o servicio relacionado." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Has actualizado a {memberName}." @@ -14721,4 +14730,3 @@ msgstr "Su código de verificación:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "su-dominio.com otro-dominio.com" - diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index fb2b175d2..a6ca6fd07 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -2441,6 +2441,7 @@ msgstr "Logo de la marque" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Préférences de branding" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Actuellement, tous les membres de l'organisation peuvent accéder à cette équipe" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Actuellement, la personnalisation de la marque ne peut être configurée que pour les plans Équipe et plus." @@ -4214,8 +4216,8 @@ msgstr "Document annulé" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Document Annulé" @@ -7942,6 +7944,11 @@ msgstr "Original" msgid "Otherwise, the document will be created as a draft." msgstr "Sinon, le document sera créé sous forme de brouillon." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "Renvoyer l’enveloppe" msgid "Resend verification" msgstr "Renvoyer la vérification" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Réinitialiser" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "Enregistrer comme modèle" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Enregistrer les modifications" @@ -10224,6 +10230,11 @@ msgstr "Paramètres du site" msgid "Skip" msgstr "Ignorer" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Certains signataires n'ont pas été assignés à un champ de signature. Veuillez assigner au moins 1 champ de signature à chaque signataire avant de continuer." @@ -12254,6 +12265,7 @@ msgstr "Non autorisé" msgid "Uncompleted" msgstr "Non complet" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Annuler" @@ -12303,6 +12315,10 @@ msgstr "Délier" msgid "Unpin" msgstr "Détacher" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "Groupe sans titre" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "Mettre à jour la bannière" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Mettre à jour la facturation" @@ -12374,10 +12388,6 @@ msgstr "Mettre à jour l'e-mail" msgid "Update Fields" msgstr "Mettre à jour les champs" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Mettre à jour l'organisation" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "Mettre à jour le rôle" msgid "Update Subscription Claim" msgstr "Mettre à jour la réclamation d'abonnement" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Mettre à jour l'équipe" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "En attente" msgid "Waiting for others" msgstr "En attente des autres" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "En attente que d'autres terminent la signature." @@ -13921,8 +13927,7 @@ msgstr "Vous avez été invité à rejoindre {0} sur Documenso" msgid "You have been invited to join the following organisation" msgstr "Vous avez été invité à rejoindre l'organisation suivante" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Vous avez été supprimé d'un document" @@ -14042,6 +14047,10 @@ msgstr "Vous avez révoqué l'accès avec succès." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Vous avez le droit de retirer votre consentement à l'utilisation des signatures électroniques à tout moment avant de terminer le processus de signature. Pour retirer votre consentement, veuillez contacter l'expéditeur du document. Si vous ne contactez pas l'expéditeur, vous pouvez contacter <0>{SUPPORT_EMAIL} pour obtenir de l'aide. Sachez que le retrait de consentement peut retarder ou arrêter l'achèvement de la transaction ou du service associé." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Vous avez mis à jour {memberName}." @@ -14721,4 +14730,3 @@ msgstr "Votre code de vérification :" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - diff --git a/packages/lib/translations/it/web.po b/packages/lib/translations/it/web.po index 1fd209ef4..af3f6de43 100644 --- a/packages/lib/translations/it/web.po +++ b/packages/lib/translations/it/web.po @@ -2441,6 +2441,7 @@ msgstr "Logo del Marchio" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Preferenze per il branding" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Attualmente tutti i membri dell'organizzazione possono accedere a questo team" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Attualmente il marchio può essere configurato solo per i piani Team e superiori." @@ -4214,8 +4216,8 @@ msgstr "Documento annullato" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Documento Annullato" @@ -7942,6 +7944,11 @@ msgstr "Originale" msgid "Otherwise, the document will be created as a draft." msgstr "Altrimenti, il documento sarà creato come bozza." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "Invia nuovamente busta" msgid "Resend verification" msgstr "Reinvia verifica" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Ripristina" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "Salva come modello" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Salva le modifiche" @@ -10224,6 +10230,11 @@ msgstr "Impostazioni del sito" msgid "Skip" msgstr "Salta" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Alcuni firmatari non hanno un campo firma assegnato. Assegna almeno 1 campo di firma a ciascun firmatario prima di procedere." @@ -12254,6 +12265,7 @@ msgstr "Non autorizzato" msgid "Uncompleted" msgstr "Incompleto" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Annulla" @@ -12303,6 +12315,10 @@ msgstr "Scollega" msgid "Unpin" msgstr "Rimuovi" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "Gruppo senza nome" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "Aggiorna banner" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Aggiorna fatturazione" @@ -12374,10 +12388,6 @@ msgstr "Aggiorna email" msgid "Update Fields" msgstr "Aggiorna campi" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Aggiorna organizzazione" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "Aggiorna ruolo" msgid "Update Subscription Claim" msgstr "Aggiorna reclamo di sottoscrizione" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Aggiorna team" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "In attesa" msgid "Waiting for others" msgstr "In attesa di altri" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "In attesa che altri completino la firma." @@ -13921,8 +13927,7 @@ msgstr "Sei stato invitato a unirti a {0} su Documenso" msgid "You have been invited to join the following organisation" msgstr "Sei stato invitato a unirti alla seguente organizzazione" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Sei stato rimosso da un documento" @@ -14042,6 +14047,10 @@ msgstr "Hai revocato con successo l'accesso." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Hai il diritto di ritirare il tuo consenso all'uso delle firme elettroniche in qualsiasi momento prima di completare il processo di firma. Per ritirare il tuo consenso, contatta il mittente del documento. Nel caso in cui non riesci a contattare il mittente, puoi contattare <0>{SUPPORT_EMAIL} per assistenza. Sii consapevole che il ritiro del consenso potrebbe ritardare o fermare il completamento della transazione o del servizio correlato." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Hai aggiornato {memberName}." @@ -14721,4 +14730,3 @@ msgstr "Il tuo codice di verifica:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "tuo-dominio.com altro-dominio.com" - diff --git a/packages/lib/translations/ja/web.po b/packages/lib/translations/ja/web.po index f2fc95818..a71b944ec 100644 --- a/packages/lib/translations/ja/web.po +++ b/packages/lib/translations/ja/web.po @@ -2441,6 +2441,7 @@ msgstr "ブランディングロゴ" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "ブランディング設定" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "現在、すべての組織メンバーがこのチームにアクセスできます" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "現在、ブランディングは Teams プラン以上のみ設定できます。" @@ -4214,8 +4216,8 @@ msgstr "文書は取り消されました" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "文書はキャンセルされました" @@ -7942,6 +7944,11 @@ msgstr "オリジナル" msgid "Otherwise, the document will be created as a draft." msgstr "チェックを入れない場合、文書は下書きとして作成されます。" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "封筒を再送信" msgid "Resend verification" msgstr "認証を再送" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "リセット" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "テンプレートとして保存" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "変更を保存" @@ -10224,6 +10230,11 @@ msgstr "サイト設定" msgid "Skip" msgstr "スキップ" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "一部の署名者に署名フィールドが割り当てられていません。続行する前に、各署名者に少なくとも 1 つの署名フィールドを割り当ててください。" @@ -12254,6 +12265,7 @@ msgstr "権限がありません" msgid "Uncompleted" msgstr "未完了" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "元に戻す" @@ -12303,6 +12315,10 @@ msgstr "リンク解除" msgid "Unpin" msgstr "ピン留めを解除" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "無題のグループ" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "バナーを更新" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "請求情報を更新" @@ -12374,10 +12388,6 @@ msgstr "メールを更新" msgid "Update Fields" msgstr "フィールドを更新" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "組織を更新" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "役割を更新" msgid "Update Subscription Claim" msgstr "サブスクリプションクレームを更新" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "チームを更新" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "保留中" msgid "Waiting for others" msgstr "他の人の完了待ち" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "他の署名者による署名完了を待っています。" @@ -13921,8 +13927,7 @@ msgstr "Documenso で {0} に参加するよう招待されています" msgid "You have been invited to join the following organisation" msgstr "次の組織に参加するよう招待されています。" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "ドキュメントから削除されました" @@ -14042,6 +14047,10 @@ msgstr "アクセスを正常に取り消しました。" msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "署名プロセスを完了する前であれば、電子署名の利用に対する同意をいつでも撤回する権利があります。同意を撤回するには、文書の送信者に連絡してください。送信者に連絡できない場合は、<0>{SUPPORT_EMAIL} までお問い合わせください。同意を撤回すると、関連する取引やサービスの完了が遅延または中止される可能性がある点にご注意ください。" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "{memberName} を更新しました。" @@ -14721,4 +14730,3 @@ msgstr "認証コード:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - diff --git a/packages/lib/translations/ko/web.po b/packages/lib/translations/ko/web.po index 743a6ef9a..f7844a294 100644 --- a/packages/lib/translations/ko/web.po +++ b/packages/lib/translations/ko/web.po @@ -2441,6 +2441,7 @@ msgstr "브랜딩 로고" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "브랜딩 환경설정" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "현재 모든 조직 구성원이 이 팀에 접근할 수 있습니다." #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "브랜딩은 현재 Teams 요금제 이상에서만 구성할 수 있습니다." @@ -4214,8 +4216,8 @@ msgstr "문서가 취소되었습니다" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "문서가 취소됨" @@ -7942,6 +7944,11 @@ msgstr "원본" msgid "Otherwise, the document will be created as a draft." msgstr "그렇지 않으면 문서는 초안으로 생성됩니다." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "봉투 다시 보내기" msgid "Resend verification" msgstr "인증 다시 보내기" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "초기화" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "템플릿으로 저장" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "변경 사항 저장" @@ -10224,6 +10230,11 @@ msgstr "사이트 설정" msgid "Skip" msgstr "건너뛰기" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "일부 서명자에게 서명 필드가 할당되지 않았습니다. 진행하기 전에 각 서명자에게 최소 1개 이상의 서명 필드를 할당해 주세요." @@ -12254,6 +12265,7 @@ msgstr "권한이 없습니다" msgid "Uncompleted" msgstr "미완료" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "실행 취소" @@ -12303,6 +12315,10 @@ msgstr "연결 해제" msgid "Unpin" msgstr "고정 해제" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "제목 없는 그룹" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "배너 업데이트" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "결제 정보 업데이트" @@ -12374,10 +12388,6 @@ msgstr "이메일 업데이트" msgid "Update Fields" msgstr "필드 업데이트" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "조직 업데이트" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "역할 업데이트" msgid "Update Subscription Claim" msgstr "구독 클레임 업데이트" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "팀 업데이트" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "대기 중" msgid "Waiting for others" msgstr "다른 사람을 기다리는 중" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "다른 서명자들이 이 문서에 서명 완료하기를 기다리는 중입니다." @@ -13921,8 +13927,7 @@ msgstr "Documenso에서 {0} 조직에 초대되었습니다." msgid "You have been invited to join the following organisation" msgstr "다음 조직에 참여하라는 초대를 받았습니다." -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "문서에서 제거되었습니다." @@ -14042,6 +14047,10 @@ msgstr "접근 권한을 성공적으로 철회했습니다." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "전자 서명을 완료하기 전 언제든지 전자 서명 사용에 대한 동의를 철회할 권리가 있습니다. 동의를 철회하려면 문서 발송자에게 문의해 주세요. 발송자에게 연락할 수 없는 경우 <0>{SUPPORT_EMAIL}로 도움을 요청해 주세요. 동의를 철회하면 관련 거래나 서비스가 지연되거나 중단될 수 있습니다." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "{memberName}을(를) 업데이트했습니다." @@ -14721,4 +14730,3 @@ msgstr "인증 코드:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - diff --git a/packages/lib/translations/nl/web.po b/packages/lib/translations/nl/web.po index 4c8da7804..0eedbcad7 100644 --- a/packages/lib/translations/nl/web.po +++ b/packages/lib/translations/nl/web.po @@ -2441,6 +2441,7 @@ msgstr "Branding-logo" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Brandingvoorkeuren" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Momenteel hebben alle organisatieleden toegang tot dit team" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Branding kan momenteel alleen worden geconfigureerd voor Teams- en hogere abonnementen." @@ -4214,8 +4216,8 @@ msgstr "Document geannuleerd" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Document geannuleerd" @@ -7942,6 +7944,11 @@ msgstr "Origineel" msgid "Otherwise, the document will be created as a draft." msgstr "Anders wordt het document als concept aangemaakt." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "Envelope opnieuw verzenden" msgid "Resend verification" msgstr "Verificatie opnieuw verzenden" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Resetten" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "Opslaan als sjabloon" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Wijzigingen opslaan" @@ -10224,6 +10230,11 @@ msgstr "Site‑instellingen" msgid "Skip" msgstr "Overslaan" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Sommige ondertekenaars hebben geen handtekeningveld toegewezen gekregen. Wijs ten minste 1 handtekeningveld toe aan elke ondertekenaar voordat je doorgaat." @@ -12254,6 +12265,7 @@ msgstr "Niet gemachtigd" msgid "Uncompleted" msgstr "Onvoltooid" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Ongedaan maken" @@ -12303,6 +12315,10 @@ msgstr "Ontkoppelen" msgid "Unpin" msgstr "Losmaken" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "Naamloze groep" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "Banner bijwerken" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Facturering bijwerken" @@ -12374,10 +12388,6 @@ msgstr "E-mail bijwerken" msgid "Update Fields" msgstr "Velden bijwerken" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Organisatie bijwerken" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "Rol bijwerken" msgid "Update Subscription Claim" msgstr "Abonnementsclaim bijwerken" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Team bijwerken" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "Wachten" msgid "Waiting for others" msgstr "Wachten op anderen" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Wachten tot anderen het ondertekenen hebben voltooid." @@ -13921,8 +13927,7 @@ msgstr "Je bent uitgenodigd om {0} op Documenso te joinen" msgid "You have been invited to join the following organisation" msgstr "Je bent uitgenodigd om lid te worden van de volgende organisatie" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Je bent verwijderd uit een document" @@ -14042,6 +14047,10 @@ msgstr "Je hebt de toegang succesvol ingetrokken." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Je hebt het recht je toestemming voor het gebruik van elektronische handtekeningen op elk moment vóór voltooiing van het ondertekeningsproces in te trekken. Neem hiervoor contact op met de verzender van het document. Als dat niet lukt, kun je contact opnemen met <0>{SUPPORT_EMAIL} voor hulp. Houd er rekening mee dat het intrekken van toestemming de voltooiing van de betreffende transactie of dienst kan vertragen of stopzetten." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Je hebt {memberName} bijgewerkt." @@ -14721,4 +14730,3 @@ msgstr "Uw verificatiecode:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index de84f4a84..4c6cd062f 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -2441,6 +2441,7 @@ msgstr "Logo marki" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Ustawienia brandingu" @@ -2501,7 +2502,6 @@ msgstr "Akceptując prośbę, przyznasz zespołowi {0} następujące uprawnienia #: packages/email/templates/confirm-team-email.tsx msgid "By accepting this request, you will be granting <0>{teamName} access to:" -msgstr "Akceptując prośbę, umożliwisz zespołowi <0>{teamName} na:" msgstr "Akceptując prośbę, umożliwisz zespołowi <0>{teamName}:" #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx @@ -3573,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "Obecnie wszyscy użytkownicy organizacji mogą uzyskać dostęp tego zespołu" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Branding możesz skonfigurować tylko w planie Teams i wyższym." @@ -4215,8 +4216,8 @@ msgstr "Anulowano dokument" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Dokument został anulowany" @@ -7943,6 +7944,11 @@ msgstr "Oryginalny" msgid "Otherwise, the document will be created as a draft." msgstr "W przeciwnym razie dokument zostanie utworzony jako wersja robocza." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9198,9 +9204,7 @@ msgstr "Wyślij ponownie kopertę" msgid "Resend verification" msgstr "Wyślij ponownie wiadomość weryfikacyjną" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Resetuj" @@ -9385,6 +9389,7 @@ msgid "Save as Template" msgstr "Zapisz jako szablon" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "Zapisz zmiany" @@ -10225,6 +10230,11 @@ msgstr "Ustawienia strony" msgid "Skip" msgstr "Pomiń" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Niektórym podpisującym nie przypisano pola podpisu. Przypisz co najmniej jedno pole podpisu do każdego podpisującego." @@ -12255,6 +12265,7 @@ msgstr "Nieautoryzowany" msgid "Uncompleted" msgstr "Niezakończono" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "Cofnij" @@ -12304,6 +12315,10 @@ msgstr "Rozłącz" msgid "Unpin" msgstr "Odepnij" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12323,9 +12338,6 @@ msgstr "Grupa bez nazwy" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12348,6 +12360,7 @@ msgstr "Zaktualizuj baner" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Zaktualizuj płatności" @@ -12375,10 +12388,6 @@ msgstr "Zaktualizuj adres e-mail" msgid "Update Fields" msgstr "Zaktualizuj pola" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Zaktualizuj organizację" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12414,10 +12423,6 @@ msgstr "Zaktualizuj rolę" msgid "Update Subscription Claim" msgstr "Zaktualizuj subskrypcję" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Zaktualizuj zespół" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12948,7 +12953,7 @@ msgstr "Oczekiwanie" msgid "Waiting for others" msgstr "Oczekiwanie na innych" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Oczekiwanie na zakończenie podpisywania przez innych." @@ -13922,8 +13927,7 @@ msgstr "Dołącz do organizacji {0} w Documenso" msgid "You have been invited to join the following organisation" msgstr "Masz zaproszenie do dołączenia do następującej organizacji" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Usunięto Cię z dokumentu" @@ -14043,6 +14047,10 @@ msgstr "Dostęp został unieważniony." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Masz prawo wycofać swoją zgodę na używanie podpisów elektronicznych w dowolnym momencie przed zakończeniem procesu podpisywania. Aby wycofać zgodę, skontaktuj się z nadawcą dokumentu. Jeśli nie możesz skontaktować się z nadawcą, napisz do nas na adres <0>{SUPPORT_EMAIL}. Pamiętaj, że wycofanie zgody może opóźnić lub wstrzymać realizację danej transakcji lub usługi." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Użytkownik {memberName} został zaktualizowany." @@ -14722,4 +14730,3 @@ msgstr "Twój kod weryfikacyjny:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "twoja-domena.pl inna-domena.pl" - diff --git a/packages/lib/translations/pt-BR/web.po b/packages/lib/translations/pt-BR/web.po index 027ae4255..ba02bfa14 100644 --- a/packages/lib/translations/pt-BR/web.po +++ b/packages/lib/translations/pt-BR/web.po @@ -2436,6 +2436,7 @@ msgstr "Logo da Marca" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "Preferências da Marca" @@ -3567,6 +3568,7 @@ msgid "Currently all organisation members can access this team" msgstr "Atualmente, todos os membros da organização podem acessar esta equipe" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "Atualmente, a marca só pode ser configurada para planos Teams e superiores." @@ -4209,8 +4211,8 @@ msgstr "" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "Documento Cancelado" @@ -7937,6 +7939,11 @@ msgstr "Original" msgid "Otherwise, the document will be created as a draft." msgstr "Caso contrário, o documento será criado como um rascunho." +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -8772,6 +8779,10 @@ msgstr "" msgid "Recipient rejected the document" msgstr "" +#: packages/lib/utils/document-audit-logs.ts +msgid "Recipient rejected the document externally" +msgstr "" + #: apps/remix/app/components/general/admin-global-settings-section.tsx msgid "Recipient removed" msgstr "" @@ -9188,9 +9199,7 @@ msgstr "" msgid "Resend verification" msgstr "Reenviar verificação" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "Redefinir" @@ -9375,6 +9384,7 @@ msgid "Save as Template" msgstr "" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "" @@ -10215,6 +10225,11 @@ msgstr "Configurações do Site" msgid "Skip" msgstr "Pular" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "Alguns signatários não receberam um campo de assinatura. Por favor, atribua pelo menos 1 campo de assinatura a cada signatário antes de prosseguir." @@ -11093,6 +11108,23 @@ msgstr "" msgid "The document was created but could not be sent to recipients." msgstr "O documento foi criado, mas não pôde ser enviado aos destinatários." +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally by {onBehalfOf} on behalf of {user}" +msgstr "" + +#: packages/lib/utils/document-audit-logs.ts +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally by {onBehalfOf} on behalf of the recipient" +msgstr "" + +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally on behalf of {user}" +msgstr "" + +#: packages/lib/utils/document-audit-logs.ts +msgid "The document was rejected externally on behalf of the recipient" +msgstr "" + #: apps/remix/app/components/dialogs/envelope-delete-dialog.tsx msgid "The document will be hidden from your account" msgstr "O documento será ocultado da sua conta" @@ -12228,6 +12260,7 @@ msgstr "Não autorizado" msgid "Uncompleted" msgstr "Não concluído" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "" @@ -12277,6 +12310,10 @@ msgstr "Desvincular" msgid "Unpin" msgstr "Desafixar" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12296,9 +12333,6 @@ msgstr "Grupo sem título" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12321,6 +12355,7 @@ msgstr "Atualizar Banner" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "Atualizar Faturamento" @@ -12348,10 +12383,6 @@ msgstr "Atualizar e-mail" msgid "Update Fields" msgstr "Atualizar Campos" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "Atualizar organização" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12387,10 +12418,6 @@ msgstr "Atualizar função" msgid "Update Subscription Claim" msgstr "Atualizar Reivindicação de Assinatura" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "Atualizar equipe" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12921,7 +12948,7 @@ msgstr "Aguardando" msgid "Waiting for others" msgstr "Aguardando outros" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "Aguardando outros completarem a assinatura." @@ -13895,8 +13922,7 @@ msgstr "Você foi convidado para participar de {0} no Documenso" msgid "You have been invited to join the following organisation" msgstr "Você foi convidado para participar da seguinte organização" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "Você foi removido de um documento" @@ -14016,6 +14042,10 @@ msgstr "Você revogou o acesso com sucesso." msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "Você tem o direito de retirar seu consentimento para usar assinaturas eletrônicas a qualquer momento antes de concluir o processo de assinatura. Para retirar seu consentimento, entre em contato com o remetente do documento. Se não conseguir entrar em contato com o remetente, você pode entrar em contato com <0>{SUPPORT_EMAIL} para obter assistência. Esteja ciente de que a retirada do consentimento pode atrasar ou interromper a conclusão da transação ou serviço relacionado." +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "Você atualizou {memberName}." diff --git a/packages/lib/translations/zh/web.po b/packages/lib/translations/zh/web.po index 975a3583c..1f191746c 100644 --- a/packages/lib/translations/zh/web.po +++ b/packages/lib/translations/zh/web.po @@ -2441,6 +2441,7 @@ msgstr "品牌 Logo" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Branding Preferences" msgstr "品牌偏好设置" @@ -3572,6 +3573,7 @@ msgid "Currently all organisation members can access this team" msgstr "目前所有组织成员都可以访问此团队" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Currently branding can only be configured for Teams and above plans." msgstr "目前仅 Teams 及以上套餐可以配置品牌。" @@ -4214,8 +4216,8 @@ msgstr "文档已被取消" #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx #: apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx +#: packages/lib/jobs/definitions/emails/send-document-deleted-emails.handler.ts #: packages/lib/server-only/admin/admin-super-delete-document.ts -#: packages/lib/server-only/document/delete-document.ts msgid "Document Cancelled" msgstr "文档已取消" @@ -7942,6 +7944,11 @@ msgstr "原始" msgid "Otherwise, the document will be created as a draft." msgstr "否则将把文档创建为草稿。" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Overlapping fields detected" +msgstr "" + #: apps/remix/app/components/forms/document-preferences-form.tsx #: apps/remix/app/components/forms/email-preferences-form.tsx msgid "Override organisation settings" @@ -9197,9 +9204,7 @@ msgstr "重新发送信封" msgid "Resend verification" msgstr "重新发送验证" -#: apps/remix/app/components/forms/organisation-update-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx -#: apps/remix/app/components/forms/team-update-form.tsx #: apps/remix/app/components/general/organisation-usage-reset-button.tsx msgid "Reset" msgstr "重置" @@ -9384,6 +9389,7 @@ msgid "Save as Template" msgstr "另存为模板" #: apps/remix/app/components/dialogs/email-transport-update-dialog.tsx +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx msgid "Save changes" msgstr "保存更改" @@ -10224,6 +10230,11 @@ msgstr "站点设置" msgid "Skip" msgstr "跳过" +#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx +msgid "Some fields are placed on top of each other. This may complicate the signing process or cause fields to not work as expected." +msgstr "" + #: packages/ui/primitives/document-flow/missing-signature-field-dialog.tsx msgid "Some signers have not been assigned a signature field. Please assign at least 1 signature field to each signer before proceeding." msgstr "部分签署人尚未被分配签名字段。请在继续前为每位签署人至少分配 1 个签名字段。" @@ -12254,6 +12265,7 @@ msgstr "未授权" msgid "Uncompleted" msgstr "未完成" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx #: packages/ui/primitives/signature-pad/signature-pad-draw.tsx msgid "Undo" msgstr "撤销" @@ -12303,6 +12315,10 @@ msgstr "取消关联" msgid "Unpin" msgstr "取消固定" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "Unsaved changes" +msgstr "" + #: apps/remix/app/routes/_authenticated+/admin+/_layout.tsx #: apps/remix/app/routes/_authenticated+/admin+/unsealed-documents._index.tsx msgid "Unsealed Documents" @@ -12322,9 +12338,6 @@ msgstr "未命名组" #: apps/remix/app/components/dialogs/team-group-update-dialog.tsx #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx #: apps/remix/app/components/dialogs/webhook-edit-dialog.tsx -#: apps/remix/app/components/forms/branding-preferences-form.tsx -#: apps/remix/app/components/forms/document-preferences-form.tsx -#: apps/remix/app/components/forms/email-preferences-form.tsx #: apps/remix/app/components/forms/public-profile-form.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-settings-dialog.tsx #: apps/remix/app/components/tables/admin-claims-table.tsx @@ -12347,6 +12360,7 @@ msgstr "更新横幅" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email-domains._index.tsx +#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx msgid "Update Billing" msgstr "更新计费" @@ -12374,10 +12388,6 @@ msgstr "更新邮箱" msgid "Update Fields" msgstr "更新字段" -#: apps/remix/app/components/forms/organisation-update-form.tsx -msgid "Update organisation" -msgstr "更新组织" - #: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx #: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx @@ -12413,10 +12423,6 @@ msgstr "更新角色" msgid "Update Subscription Claim" msgstr "更新订阅声明" -#: apps/remix/app/components/forms/team-update-form.tsx -msgid "Update team" -msgstr "更新团队" - #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx #: apps/remix/app/components/dialogs/team-email-update-dialog.tsx msgid "Update team email" @@ -12947,7 +12953,7 @@ msgstr "等待中" msgid "Waiting for others" msgstr "等待其他人" -#: packages/lib/server-only/document/send-pending-email.ts +#: packages/lib/jobs/definitions/emails/send-document-pending-email.handler.ts msgid "Waiting for others to complete signing." msgstr "正在等待其他人完成签署。" @@ -13921,8 +13927,7 @@ msgstr "您已被邀请加入 Documenso 上的 {0}" msgid "You have been invited to join the following organisation" msgstr "您已被邀请加入以下组织" -#: packages/lib/server-only/recipient/delete-envelope-recipient.ts -#: packages/lib/server-only/recipient/set-document-recipients.ts +#: packages/lib/jobs/definitions/emails/send-recipient-removed-email.handler.ts msgid "You have been removed from a document" msgstr "您已被从某个文档中移除" @@ -14042,6 +14047,10 @@ msgstr "你已成功撤销访问权限。" msgid "You have the right to withdraw your consent to use electronic signatures at any time before completing the signing process. To withdraw your consent, please contact the sender of the document. In failing to contact the sender you may reach out to <0>{SUPPORT_EMAIL} for assistance. Be aware that withdrawing consent may delay or halt the completion of the related transaction or service." msgstr "在完成签署流程之前,你有权随时撤回对使用电子签名的同意。要撤回同意,请联系文档的发送方。如果无法联系发送方,你可以通过 <0>{SUPPORT_EMAIL} 与我们联系以获得协助。请注意,撤回同意可能会延迟或中止相关交易或服务的完成。" +#: apps/remix/app/components/forms/form-sticky-save-bar.tsx +msgid "You have unsaved changes" +msgstr "" + #: apps/remix/app/components/dialogs/team-member-update-dialog.tsx msgid "You have updated {memberName}." msgstr "您已更新 {memberName}。" @@ -14721,4 +14730,3 @@ msgstr "您的验证码:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" - From 337f85f02119fef19ccec7ff0a208cfc95ba5bd7 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 2 Jul 2026 15:09:07 +1000 Subject: [PATCH 21/41] chore: upgrade libpdf (#3058) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb4e523f1..d86982437 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "dependencies": { "@ai-sdk/google-vertex": "3.0.81", "@documenso/prisma": "*", - "@libpdf/core": "^0.4.0", + "@libpdf/core": "^0.4.1", "@lingui/conf": "^5.6.0", "@lingui/core": "^5.6.0", "@marsidev/react-turnstile": "^1.5.0", @@ -4661,9 +4661,9 @@ "license": "MIT" }, "node_modules/@libpdf/core": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.4.0.tgz", - "integrity": "sha512-G9nZRjf9DGDJaS/C23YWogk8akPM7O/6HfMslxVsKTKRbbbb+0szpQIetcGGUGRu7KtmBDmGDWCgz//DXSmq8A==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@libpdf/core/-/core-0.4.1.tgz", + "integrity": "sha512-DWGxWw1na8oFPixz+b6kOgu4tMD8xJLDgyPGVUNqIswO5POHAxsqmTvUvDW0IrwHP7kFRHBV3I3T/KhTABf9rA==", "license": "MIT", "dependencies": { "@noble/ciphers": "^2.2.0", diff --git a/package.json b/package.json index d0ffcb2b5..27e69e864 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "dependencies": { "@ai-sdk/google-vertex": "3.0.81", "@documenso/prisma": "*", - "@libpdf/core": "^0.4.0", + "@libpdf/core": "^0.4.1", "@lingui/conf": "^5.6.0", "@lingui/core": "^5.6.0", "@prisma/extension-read-replicas": "^0.4.1", From d35d13db23dbff7e4abed63d50f3b4fea2e5e1b3 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 2 Jul 2026 15:51:19 +1000 Subject: [PATCH 22/41] fix: remove presigned branding upload (#3053) --- .../forms/branding-preferences-form.tsx | 17 +- .../o.$orgUrl.settings.branding.tsx | 20 +- .../t.$teamUrl+/settings.branding.tsx | 19 +- apps/remix/server/api/files/files.ts | 29 +-- apps/remix/server/api/files/files.types.ts | 21 -- apps/remix/server/router.ts | 1 - .../api-access-file-upload.spec.ts | 40 ---- .../e2e/branding-logo-optimise.spec.ts | 37 +++ .../e2e/branding-logo-upload.spec.ts | 225 ++++++++++++++++++ .../app-tests/e2e/signing-branding.spec.ts | 35 +++ packages/lib/constants/branding.ts | 10 + .../branding/store-branding-logo.ts | 26 ++ packages/lib/universal/upload/put-file.ts | 72 +----- packages/lib/utils/images/logo.ts | 12 + .../trpc/server/organisation-router/router.ts | 2 + .../update-organisation-branding-logo.ts | 69 ++++++ ...update-organisation-branding-logo.types.ts | 17 ++ .../update-organisation-settings.ts | 2 - .../update-organisation-settings.types.ts | 1 - packages/trpc/server/team-router/router.ts | 2 + .../team-router/update-team-branding-logo.ts | 69 ++++++ .../update-team-branding-logo.types.ts | 17 ++ .../team-router/update-team-settings.ts | 2 - .../team-router/update-team-settings.types.ts | 1 - packages/trpc/utils/zod-form-data.ts | 20 ++ 25 files changed, 575 insertions(+), 191 deletions(-) create mode 100644 packages/app-tests/e2e/branding-logo-optimise.spec.ts create mode 100644 packages/app-tests/e2e/branding-logo-upload.spec.ts create mode 100644 packages/lib/server-only/branding/store-branding-logo.ts create mode 100644 packages/trpc/server/organisation-router/update-organisation-branding-logo.ts create mode 100644 packages/trpc/server/organisation-router/update-organisation-branding-logo.types.ts create mode 100644 packages/trpc/server/team-router/update-team-branding-logo.ts create mode 100644 packages/trpc/server/team-router/update-team-branding-logo.types.ts diff --git a/apps/remix/app/components/forms/branding-preferences-form.tsx b/apps/remix/app/components/forms/branding-preferences-form.tsx index 561fb5512..ef3ff6b34 100644 --- a/apps/remix/app/components/forms/branding-preferences-form.tsx +++ b/apps/remix/app/components/forms/branding-preferences-form.tsx @@ -1,5 +1,10 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { + BRANDING_LOGO_ALLOWED_TYPES, + BRANDING_LOGO_MAX_SIZE_BYTES, + BRANDING_LOGO_MAX_SIZE_MB, +} from '@documenso/lib/constants/branding'; import { DEFAULT_BRAND_COLORS, DEFAULT_BRAND_RADIUS } from '@documenso/lib/constants/theme'; import { ZCssVarsSchema } from '@documenso/lib/types/css-vars'; import { cn } from '@documenso/ui/lib/utils'; @@ -23,15 +28,15 @@ import { useCspNonce } from '~/utils/nonce'; import { FormStickySaveBar } from './form-sticky-save-bar'; -const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB -const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; - const ZBrandingPreferencesFormSchema = z.object({ brandingEnabled: z.boolean().nullable(), brandingLogo: z .instanceof(File) - .refine((file) => file.size <= MAX_FILE_SIZE, 'File size must be less than 5MB') - .refine((file) => ACCEPTED_FILE_TYPES.includes(file.type), 'Only .jpg, .png, and .webp files are accepted') + .refine( + (file) => file.size <= BRANDING_LOGO_MAX_SIZE_BYTES, + `File size must be less than ${BRANDING_LOGO_MAX_SIZE_MB}MB`, + ) + .refine((file) => BRANDING_LOGO_ALLOWED_TYPES.includes(file.type), 'Only .jpg, .png, and .webp files are accepted') .nullish(), brandingUrl: z.string().url().optional().or(z.literal('')), brandingCompanyDetails: z.string().max(500).optional(), @@ -245,7 +250,7 @@ export function BrandingPreferencesForm({ { const file = e.target.files?.[0]; diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx index 707c4ad96..c719f087f 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.branding.tsx @@ -1,7 +1,6 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; import { canExecuteOrganisationAction, isPersonalLayout } from '@documenso/lib/utils/organisations'; import type { SanitizeBrandingCssWarning } from '@documenso/lib/utils/sanitize-branding-css'; import { trpc } from '@documenso/trpc/react'; @@ -49,26 +48,29 @@ export default function OrganisationSettingsBrandingPage() { const { mutateAsync: updateOrganisationSettings } = trpc.organisation.settings.update.useMutation(); + const { mutateAsync: updateOrganisationBrandingLogo } = trpc.organisation.settings.updateBrandingLogo.useMutation(); + const onBrandingPreferencesFormSubmit = async (data: TBrandingPreferencesFormSchema) => { try { const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors, brandingCss } = data; - let uploadedBrandingLogo: string | undefined; + // Upload (or clear) the logo through the dedicated, server-validated route. + if (brandingLogo instanceof File || brandingLogo === null) { + const formData = new FormData(); - if (brandingLogo) { - uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo)); - } + formData.append('payload', JSON.stringify({ organisationId: organisation.id })); - // Empty the branding logo if the user unsets it. - if (brandingLogo === null) { - uploadedBrandingLogo = ''; + if (brandingLogo instanceof File) { + formData.append('brandingLogo', brandingLogo); + } + + await updateOrganisationBrandingLogo(formData); } const result = await updateOrganisationSettings({ organisationId: organisation.id, data: { brandingEnabled: brandingEnabled ?? undefined, - brandingLogo: uploadedBrandingLogo, brandingUrl, brandingCompanyDetails, brandingColors, diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx index 0401d4da2..6035941d3 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx @@ -1,6 +1,5 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; -import { putFile } from '@documenso/lib/universal/upload/put-file'; import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations'; import type { SanitizeBrandingCssWarning } from '@documenso/lib/utils/sanitize-branding-css'; import { trpc } from '@documenso/trpc/react'; @@ -38,6 +37,7 @@ export default function TeamsSettingsPage() { }); const { mutateAsync: updateTeamSettings } = trpc.team.settings.update.useMutation(); + const { mutateAsync: updateTeamBrandingLogo } = trpc.team.settings.updateBrandingLogo.useMutation(); const canConfigureBranding = organisation.organisationClaim.flags.allowCustomBranding || !IS_BILLING_ENABLED(); @@ -48,22 +48,23 @@ export default function TeamsSettingsPage() { try { const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors, brandingCss } = data; - let uploadedBrandingLogo: string | undefined; + // Upload (or clear) the logo through the dedicated, server-validated route. + if (brandingLogo instanceof File || brandingLogo === null) { + const formData = new FormData(); - if (brandingLogo) { - uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo)); - } + formData.append('payload', JSON.stringify({ teamId: team.id })); - // Empty the branding logo if the user unsets it. - if (brandingLogo === null) { - uploadedBrandingLogo = ''; + if (brandingLogo instanceof File) { + formData.append('brandingLogo', brandingLogo); + } + + await updateTeamBrandingLogo(formData); } const result = await updateTeamSettings({ teamId: team.id, data: { brandingEnabled, - brandingLogo: uploadedBrandingLogo, brandingUrl: brandingUrl || null, brandingCompanyDetails: brandingCompanyDetails || null, brandingColors, diff --git a/apps/remix/server/api/files/files.ts b/apps/remix/server/api/files/files.ts index 6885c7bfb..bbca38885 100644 --- a/apps/remix/server/api/files/files.ts +++ b/apps/remix/server/api/files/files.ts @@ -1,9 +1,8 @@ import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { AppError } from '@documenso/lib/errors/app-error'; import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server'; -import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { prisma } from '@documenso/prisma'; import { sValidator } from '@hono/standard-validator'; import type { Prisma } from '@prisma/client'; @@ -12,14 +11,11 @@ import { Hono } from 'hono'; import type { HonoEnv } from '../../router'; import { checkEnvelopeFileAccess, handleEnvelopeItemFileRequest, resolveFileUploadUserId } from './files.helpers'; import { - isAllowedUploadContentType, - type TGetPresignedPostUrlResponse, ZGetEnvelopeItemFileDownloadRequestParamsSchema, ZGetEnvelopeItemFileRequestParamsSchema, ZGetEnvelopeItemFileRequestQuerySchema, ZGetEnvelopeItemFileTokenDownloadRequestParamsSchema, ZGetEnvelopeItemFileTokenRequestParamsSchema, - ZGetPresignedPostUrlRequestSchema, ZUploadPdfRequestSchema, } from './files.types'; import getEnvelopeItemPdfRoute from './routes/get-envelope-item-pdf'; @@ -61,29 +57,6 @@ export const filesRoute = new Hono() return c.json({ error: 'Upload failed' }, 500); } }) - .post('/presigned-post-url', sValidator('json', ZGetPresignedPostUrlRequestSchema), async (c) => { - const userId = await resolveFileUploadUserId(c); - - if (!userId) { - return c.json({ error: 'Unauthorized' }, 401); - } - - const { fileName, contentType } = c.req.valid('json'); - - if (!isAllowedUploadContentType(contentType)) { - return c.json({ error: 'Unsupported content type' }, 400); - } - - try { - const { key, url } = await getPresignPostUrl(fileName, contentType, userId); - - return c.json({ key, url } satisfies TGetPresignedPostUrlResponse); - } catch (err) { - console.error(err); - - throw new AppError(AppErrorCode.UNKNOWN_ERROR); - } - }) .get( '/envelope/:envelopeId/envelopeItem/:envelopeItemId', sValidator('param', ZGetEnvelopeItemFileRequestParamsSchema), diff --git a/apps/remix/server/api/files/files.types.ts b/apps/remix/server/api/files/files.types.ts index 99f775274..28cb5ded2 100644 --- a/apps/remix/server/api/files/files.types.ts +++ b/apps/remix/server/api/files/files.types.ts @@ -13,27 +13,6 @@ export const ZUploadPdfResponseSchema = DocumentDataSchema.pick({ export type TUploadPdfRequest = z.infer; export type TUploadPdfResponse = z.infer; -export const ALLOWED_UPLOAD_CONTENT_TYPES = ['application/pdf', 'image/jpeg', 'image/png', 'image/webp'] as const; - -export const isAllowedUploadContentType = (contentType: string): boolean => { - const normalizedContentType = contentType.split(';').at(0)?.trim().toLowerCase(); - - return ALLOWED_UPLOAD_CONTENT_TYPES.some((allowed) => allowed === normalizedContentType); -}; - -export const ZGetPresignedPostUrlRequestSchema = z.object({ - fileName: z.string().min(1), - contentType: z.string().min(1), -}); - -export const ZGetPresignedPostUrlResponseSchema = z.object({ - key: z.string().min(1), - url: z.string().min(1), -}); - -export type TGetPresignedPostUrlRequest = z.infer; -export type TGetPresignedPostUrlResponse = z.infer; - export const ZGetEnvelopeItemFileRequestParamsSchema = z.object({ envelopeId: z.string().min(1), envelopeItemId: z.string().min(1), diff --git a/apps/remix/server/router.ts b/apps/remix/server/router.ts index 19747d4b2..8c9138404 100644 --- a/apps/remix/server/router.ts +++ b/apps/remix/server/router.ts @@ -105,7 +105,6 @@ app.route('/api/auth', auth); // Files route. app.use('/api/files/upload-pdf', fileRateLimitMiddleware); -app.use('/api/files/presigned-post-url', fileRateLimitMiddleware); app.route('/api/files', filesRoute); // AI route. diff --git a/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts b/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts index 21b58d737..6aac373cf 100644 --- a/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts +++ b/packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts @@ -44,46 +44,6 @@ test.describe('File upload endpoint authorization', () => { expect(res.status()).toBe(401); }); - test('rejects an unauthenticated presigned-post-url request', async ({ request }) => { - const res = await request.post(`${WEBAPP_BASE_URL}/api/files/presigned-post-url`, { - headers: { 'Content-Type': 'application/json' }, - data: { fileName: 'test.pdf', contentType: 'application/pdf' }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(401); - }); - - test('rejects a presigned-post-url request with an invalid presign token', async ({ request }) => { - const res = await request.post(`${WEBAPP_BASE_URL}/api/files/presigned-post-url`, { - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer not-a-real-token', - }, - data: { fileName: 'test.pdf', contentType: 'application/pdf' }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(401); - }); - - test('rejects a presigned-post-url request with a disallowed content type', async ({ request }) => { - const { user, team } = await seedUser(); - const presignToken = await createPresignTokenForUser(user.id, team.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/files/presigned-post-url`, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${presignToken}`, - }, - data: { fileName: 'malware.exe', contentType: 'application/x-msdownload' }, - }); - - // Authenticated, but the content type is not on the allow-list. - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(400); - }); - test('allows an upload-pdf request authorized by a valid presign token', async ({ request }) => { const { user, team } = await seedUser(); const presignToken = await createPresignTokenForUser(user.id, team.id); diff --git a/packages/app-tests/e2e/branding-logo-optimise.spec.ts b/packages/app-tests/e2e/branding-logo-optimise.spec.ts new file mode 100644 index 000000000..8f26de966 --- /dev/null +++ b/packages/app-tests/e2e/branding-logo-optimise.spec.ts @@ -0,0 +1,37 @@ +import { optimiseBrandingLogo } from '@documenso/lib/utils/images/logo'; +import { expect, test } from '@playwright/test'; +import sharp from 'sharp'; + +const makePng = async (width = 1200, height = 1200) => + sharp({ + create: { width, height, channels: 3, background: { r: 10, g: 20, b: 30 } }, + }) + .png() + .toBuffer(); + +test.describe('optimiseBrandingLogo', () => { + test('re-encodes a valid image to a PNG buffer', async () => { + const input = await makePng(); + + const output = await optimiseBrandingLogo(input); + + const metadata = await sharp(output).metadata(); + + expect(metadata.format).toBe('png'); + }); + + test('bounds the image to a maximum of 512px on its largest side', async () => { + const input = await makePng(2000, 1000); + + const output = await optimiseBrandingLogo(input); + + const metadata = await sharp(output).metadata(); + + expect(metadata.width).toBeLessThanOrEqual(512); + expect(metadata.height).toBeLessThanOrEqual(512); + }); + + test('rejects input that is not a valid image', async () => { + await expect(optimiseBrandingLogo(Buffer.from('this is not an image'))).rejects.toThrow(); + }); +}); diff --git a/packages/app-tests/e2e/branding-logo-upload.spec.ts b/packages/app-tests/e2e/branding-logo-upload.spec.ts new file mode 100644 index 000000000..2b7bd6602 --- /dev/null +++ b/packages/app-tests/e2e/branding-logo-upload.spec.ts @@ -0,0 +1,225 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { prisma } from '@documenso/prisma'; +import { seedUser } from '@documenso/prisma/seed/users'; +import { expect, type Page, test } from '@playwright/test'; + +import { apiSignin } from './fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +const LOGO_PATH = path.join(__dirname, '../../assets/logo.png'); + +type MultipartFile = { name: string; mimeType: string; buffer: Buffer }; + +const enableBrandingAndUpload = async (page: Page) => { + // Enable custom branding so the file input is no longer disabled. + await page.getByTestId('enable-branding').click(); + await page.getByRole('option', { name: 'Yes' }).click(); + + // Upload the logo file through the real multipart route. + await page.locator('input[type="file"]').setInputFiles(LOGO_PATH); + + await page.getByRole('button', { name: 'Save changes' }).first().click(); + await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible(); +}; + +/** + * POST a logo straight to the dedicated multipart tRPC route using the + * authenticated browser cookies. This bypasses the client-side form validation, + * which is the only way to exercise the server-side image validation / + * sanitisation (`zfdBrandingImageFile` + `optimiseBrandingLogo`) and the entitlement gate. + */ +const postOrganisationBrandingLogo = async (page: Page, organisationId: string, file: MultipartFile | null) => { + const multipart: Record = { + payload: JSON.stringify({ organisationId }), + }; + + if (file) { + multipart.brandingLogo = file; + } + + return await page + .context() + .request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/trpc/organisation.settings.updateBrandingLogo`, { multipart }); +}; + +/** + * Grant the organisation the custom-branding entitlement. The positive branding + * flows require it whenever billing is enabled; with billing disabled the gate is + * bypassed, so this keeps these tests valid in both modes. + */ +const grantCustomBranding = async (organisationClaimId: string) => { + await prisma.organisationClaim.update({ + where: { id: organisationClaimId }, + data: { flags: { allowLegacyEnvelopes: true, allowCustomBranding: true } }, + }); +}; + +test('[BRANDING_LOGO]: uploads an organisation branding logo via the dedicated route', async ({ page }) => { + const { user, organisation } = await seedUser({ isPersonalOrganisation: false }); + + await grantCustomBranding(organisation.organisationClaim.id); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/o/${organisation.url}/settings/branding`, + }); + + await enableBrandingAndUpload(page); + + const settings = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + expect(settings.brandingLogo).toBeTruthy(); + + const parsed = JSON.parse(settings.brandingLogo); + expect(parsed).toHaveProperty('type'); + expect(parsed).toHaveProperty('data'); +}); + +test('[BRANDING_LOGO]: uploads a team branding logo via the dedicated route', async ({ page }) => { + const { user, team, organisation } = await seedUser({ isPersonalOrganisation: false }); + + await grantCustomBranding(organisation.organisationClaim.id); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/t/${team.url}/settings/branding`, + }); + + await enableBrandingAndUpload(page); + + // TeamGlobalSettings has no `teamId` column (the FK lives on Team), so read it + // through the team relation. + const teamWithSettings = await prisma.team.findUniqueOrThrow({ + where: { id: team.id }, + include: { teamGlobalSettings: true }, + }); + + expect(teamWithSettings.teamGlobalSettings?.brandingLogo).toBeTruthy(); + + const parsed = JSON.parse(teamWithSettings.teamGlobalSettings?.brandingLogo ?? ''); + expect(parsed).toHaveProperty('type'); + expect(parsed).toHaveProperty('data'); +}); + +test('[BRANDING_LOGO]: clears the organisation branding logo when the user removes it', async ({ page }) => { + const { user, organisation } = await seedUser({ isPersonalOrganisation: false }); + + await grantCustomBranding(organisation.organisationClaim.id); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/o/${organisation.url}/settings/branding`, + }); + + await enableBrandingAndUpload(page); + + // Confirm the logo was stored before we clear it. + const settings = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + expect(settings.brandingLogo).toBeTruthy(); + + // Remove the logo and save again. + await page.getByRole('button', { name: 'Remove' }).click(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); + + // Clearing the logo persists an empty string via the dedicated route. + await expect + .poll(async () => { + const updated = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + return updated.brandingLogo; + }) + .toBe(''); +}); + +test('[BRANDING_LOGO]: validates and sanitises the logo on the server', async ({ page }) => { + const { user, organisation } = await seedUser({ isPersonalOrganisation: false }); + + await grantCustomBranding(organisation.organisationClaim.id); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/o/${organisation.url}/settings/branding`, + }); + + // Positive control: a genuine PNG is accepted and stored. This also proves the + // direct multipart request shape matches what the route expects. + const validResponse = await postOrganisationBrandingLogo(page, organisation.id, { + name: 'logo.png', + mimeType: 'image/png', + buffer: fs.readFileSync(LOGO_PATH), + }); + + expect(validResponse.ok()).toBeTruthy(); + + const afterValid = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + expect(afterValid.brandingLogo).toBeTruthy(); + + // Bytes that pass the MIME/size allowlist but are not a real image must be + // rejected by the server (the `sharp` re-encode) without changing stored state. + const invalidResponse = await postOrganisationBrandingLogo(page, organisation.id, { + name: 'fake.png', + mimeType: 'image/png', + buffer: Buffer.from('this is definitely not a valid png'), + }); + + expect(invalidResponse.ok()).toBeFalsy(); + expect(invalidResponse.status()).toBeGreaterThanOrEqual(400); + expect(invalidResponse.status()).toBeLessThan(500); + + const afterInvalid = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + // The previously stored, valid logo is left untouched by the rejected upload. + expect(afterInvalid.brandingLogo).toBe(afterValid.brandingLogo); +}); + +test('[BRANDING_LOGO]: rejects setting a logo without the custom-branding entitlement', async ({ page }) => { + // The entitlement is only enforced when billing is enabled; with billing off + // the check is intentionally skipped server-side, so this can't be exercised. + test.skip( + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED !== 'true', + 'Entitlement is only enforced when billing is enabled.', + ); + + // Seeded organisations have no `allowCustomBranding` claim flag. + const { user, organisation } = await seedUser({ isPersonalOrganisation: false }); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/o/${organisation.url}/settings/branding`, + }); + + const response = await postOrganisationBrandingLogo(page, organisation.id, { + name: 'logo.png', + mimeType: 'image/png', + buffer: fs.readFileSync(LOGO_PATH), + }); + + expect(response.ok()).toBeFalsy(); + + const settings = await prisma.organisationGlobalSettings.findUniqueOrThrow({ + where: { id: organisation.organisationGlobalSettingsId }, + }); + + expect(settings.brandingLogo).toBeFalsy(); +}); diff --git a/packages/app-tests/e2e/signing-branding.spec.ts b/packages/app-tests/e2e/signing-branding.spec.ts index 9cd9fbf85..1292d2f83 100644 --- a/packages/app-tests/e2e/signing-branding.spec.ts +++ b/packages/app-tests/e2e/signing-branding.spec.ts @@ -142,3 +142,38 @@ test('[SIGNING_BRANDING]: embedded signing does not render custom logo Brand Web await expect(page.locator(`a[href="${BRANDING_URL}"]`)).toHaveCount(0); await expect(page.getByRole('link', { name: `${team.name}'s Logo` })).toHaveCount(0); }); + +test('[SIGNING_BRANDING]: custom logo renders when branding is enabled and is hidden when disabled', async ({ + page, +}) => { + const { user, team, organisation } = await seedUser(); + + await enableOrganisationBranding({ + organisationGlobalSettingsId: organisation.organisationGlobalSettingsId, + }); + + const { recipients } = await seedPendingDocumentWithFullFields({ + owner: user, + teamId: team.id, + recipients: ['enabled-disabled-branding-signer@test.documenso.com'], + fields: [FieldType.SIGNATURE], + updateDocumentOptions: { internalVersion: 2 }, + }); + + // Branding enabled → the custom logo is rendered on the signing page. + await page.goto(`/sign/${recipients[0].token}`); + await expectPlainBrandingLogo(page, `${team.name}'s Logo`); + + // Disable branding while keeping the stored logo (the team inherits this). + await prisma.organisationGlobalSettings.update({ + where: { id: organisation.organisationGlobalSettingsId }, + data: { brandingEnabled: false }, + }); + + // Branding disabled → the custom logo is gone and the Documenso fallback + // (an internal link to "/") is shown instead. + await page.goto(`/sign/${recipients[0].token}`); + + await expect(page.getByRole('img', { name: `${team.name}'s Logo` })).toHaveCount(0); + await expect(page.locator('a[href="/"]').first()).toBeVisible(); +}); diff --git a/packages/lib/constants/branding.ts b/packages/lib/constants/branding.ts index f50cc7098..0f00237b7 100644 --- a/packages/lib/constants/branding.ts +++ b/packages/lib/constants/branding.ts @@ -9,3 +9,13 @@ * cap so a malicious or runaway payload can't exhaust PostCSS/server memory. */ export const BRANDING_CSS_MAX_LENGTH = 256 * 1024; + +/** + * Branding logo upload constraints. Enforced server-side at the TRPC request + * boundary (`zfdBrandingImageFile`) and reused by the client form for matching UX. + */ +export const BRANDING_LOGO_MAX_SIZE_MB = 5; + +export const BRANDING_LOGO_MAX_SIZE_BYTES = BRANDING_LOGO_MAX_SIZE_MB * 1024 * 1024; + +export const BRANDING_LOGO_ALLOWED_TYPES: string[] = ['image/jpeg', 'image/png', 'image/webp']; diff --git a/packages/lib/server-only/branding/store-branding-logo.ts b/packages/lib/server-only/branding/store-branding-logo.ts new file mode 100644 index 000000000..af0601aec --- /dev/null +++ b/packages/lib/server-only/branding/store-branding-logo.ts @@ -0,0 +1,26 @@ +import { AppError, AppErrorCode } from '../../errors/app-error'; +import { putFileServerSide } from '../../universal/upload/put-file.server'; +import { optimiseBrandingLogo } from '../../utils/images/logo'; + +/** + * Validate, sanitise and store an uploaded branding logo. Returns the + * `JSON.stringify({ type, data })` reference persisted in the `brandingLogo` + * column (the same format the serving endpoints already expect). + */ +export const buildBrandingLogoData = async (file: File): Promise => { + const buffer = Buffer.from(await file.arrayBuffer()); + + const optimised = await optimiseBrandingLogo(buffer).catch(() => { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'The branding logo must be a valid image file.', + }); + }); + + const documentData = await putFileServerSide({ + name: 'branding-logo.png', + type: 'image/png', + arrayBuffer: async () => Promise.resolve(optimised), + }); + + return JSON.stringify(documentData); +}; diff --git a/packages/lib/universal/upload/put-file.ts b/packages/lib/universal/upload/put-file.ts index 5a7eeecc0..46b53a861 100644 --- a/packages/lib/universal/upload/put-file.ts +++ b/packages/lib/universal/upload/put-file.ts @@ -1,10 +1,5 @@ -import { env } from '@documenso/lib/utils/env'; -import type { TGetPresignedPostUrlResponse, TUploadPdfResponse } from '@documenso/remix/server/api/files/files.types'; -import { DocumentDataType } from '@prisma/client'; -import { base64 } from '@scure/base'; -import { match } from 'ts-pattern'; +import type { TUploadPdfResponse } from '@documenso/remix/server/api/files/files.types'; -import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { AppError } from '../../errors/app-error'; type File = { @@ -58,68 +53,3 @@ export const putPdfFile = async (file: File, options?: PutFileOptions) => { return result; }; - -/** - * Uploads a file to the appropriate storage location. - */ -export const putFile = async (file: File, options?: PutFileOptions) => { - const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT'); - - return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT) - .with('s3', async () => putFileInObjectStorage(file, {}, options)) - .with('azure-blob', async () => putFileInObjectStorage(file, { 'x-ms-blob-type': 'BlockBlob' }, options)) - .otherwise(async () => putFileInDatabase(file)); -}; - -const putFileInDatabase = async (file: File) => { - const contents = await file.arrayBuffer(); - - const binaryData = new Uint8Array(contents); - - const asciiData = base64.encode(binaryData); - - return { - type: DocumentDataType.BYTES_64, - data: asciiData, - }; -}; - -const putFileInObjectStorage = async (file: File, extraHeaders: Record, options?: PutFileOptions) => { - const getPresignedUrlResponse = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/files/presigned-post-url`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...buildUploadAuthHeaders(options), - }, - body: JSON.stringify({ - fileName: file.name, - contentType: file.type, - }), - }); - - if (!getPresignedUrlResponse.ok) { - throw new Error(`Failed to get presigned post url, failed with status code ${getPresignedUrlResponse.status}`); - } - - const { url, key }: TGetPresignedPostUrlResponse = await getPresignedUrlResponse.json(); - - const body = await file.arrayBuffer(); - - const response = await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/octet-stream', - ...extraHeaders, - }, - body, - }); - - if (!response.ok) { - throw new Error(`Failed to upload file "${file.name}", failed with status code ${response.status}`); - } - - return { - type: DocumentDataType.S3_PATH, - data: key, - }; -}; diff --git a/packages/lib/utils/images/logo.ts b/packages/lib/utils/images/logo.ts index b81f7c012..98b2edebb 100644 --- a/packages/lib/utils/images/logo.ts +++ b/packages/lib/utils/images/logo.ts @@ -8,3 +8,15 @@ export const loadLogo = async (file: Uint8Array) => { content, }; }; + +/** + * Validate and sanitise an uploaded branding logo. Re-encoding through `sharp` + * proves the bytes are a real raster image and strips any embedded payloads. + * Throws if the input cannot be parsed as an image. + */ +export const optimiseBrandingLogo = async (input: Buffer | Uint8Array): Promise => { + return await sharp(input) + .resize(512, 512, { fit: 'inside', withoutEnlargement: true }) + .png({ quality: 80 }) + .toBuffer(); +}; diff --git a/packages/trpc/server/organisation-router/router.ts b/packages/trpc/server/organisation-router/router.ts index 3a66faab0..e0c1bbba4 100644 --- a/packages/trpc/server/organisation-router/router.ts +++ b/packages/trpc/server/organisation-router/router.ts @@ -20,6 +20,7 @@ import { getOrganisationsRoute } from './get-organisations'; import { leaveOrganisationRoute } from './leave-organisation'; import { resendOrganisationMemberInviteRoute } from './resend-organisation-member-invite'; import { updateOrganisationRoute } from './update-organisation'; +import { updateOrganisationBrandingLogoRoute } from './update-organisation-branding-logo'; import { updateOrganisationGroupRoute } from './update-organisation-group'; import { updateOrganisationMemberRoute } from './update-organisation-members'; import { updateOrganisationSettingsRoute } from './update-organisation-settings'; @@ -55,6 +56,7 @@ export const organisationRouter = router({ }, settings: { update: updateOrganisationSettingsRoute, + updateBrandingLogo: updateOrganisationBrandingLogoRoute, }, internal: { getOrganisationSession: getOrganisationSessionRoute, diff --git a/packages/trpc/server/organisation-router/update-organisation-branding-logo.ts b/packages/trpc/server/organisation-router/update-organisation-branding-logo.ts new file mode 100644 index 000000000..a4ee8ffcd --- /dev/null +++ b/packages/trpc/server/organisation-router/update-organisation-branding-logo.ts @@ -0,0 +1,69 @@ +import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; +import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { buildBrandingLogoData } from '@documenso/lib/server-only/branding/store-branding-logo'; +import { getOrganisationClaim } from '@documenso/lib/server-only/organisation/get-organisation-claims'; +import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations'; +import { prisma } from '@documenso/prisma'; + +import { authenticatedProcedure } from '../trpc'; +import { + ZUpdateOrganisationBrandingLogoRequestSchema, + ZUpdateOrganisationBrandingLogoResponseSchema, +} from './update-organisation-branding-logo.types'; + +export const updateOrganisationBrandingLogoRoute = authenticatedProcedure + .input(ZUpdateOrganisationBrandingLogoRequestSchema) + .output(ZUpdateOrganisationBrandingLogoResponseSchema) + .mutation(async ({ ctx, input }) => { + const { user } = ctx; + const { payload, brandingLogo } = input; + const { organisationId } = payload; + + ctx.logger.info({ + input: { + organisationId, + }, + }); + + const organisation = await prisma.organisation.findFirst({ + where: buildOrganisationWhereQuery({ + organisationId, + userId: user.id, + roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'], + }), + }); + + if (!organisation) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You do not have permission to update this organisation.', + }); + } + + // Setting a logo requires the custom-branding entitlement; clearing it is + // always allowed so a downgraded organisation can still remove its logo. + if (brandingLogo && IS_BILLING_ENABLED()) { + const claim = await getOrganisationClaim({ organisationId }); + + if (claim.flags?.allowCustomBranding !== true) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Your plan does not allow custom branding.', + }); + } + } + + const brandingLogoValue = brandingLogo ? await buildBrandingLogoData(brandingLogo) : ''; + + await prisma.organisation.update({ + where: { + id: organisation.id, + }, + data: { + organisationGlobalSettings: { + update: { + brandingLogo: brandingLogoValue, + }, + }, + }, + }); + }); diff --git a/packages/trpc/server/organisation-router/update-organisation-branding-logo.types.ts b/packages/trpc/server/organisation-router/update-organisation-branding-logo.types.ts new file mode 100644 index 000000000..059902921 --- /dev/null +++ b/packages/trpc/server/organisation-router/update-organisation-branding-logo.types.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { zfd } from 'zod-form-data'; + +import { zfdBrandingImageFile, zodFormData } from '../../utils/zod-form-data'; + +export const ZUpdateOrganisationBrandingLogoRequestSchema = zodFormData({ + payload: zfd.json( + z.object({ + organisationId: z.string(), + }), + ), + brandingLogo: zfdBrandingImageFile().optional(), +}); + +export const ZUpdateOrganisationBrandingLogoResponseSchema = z.void(); + +export type TUpdateOrganisationBrandingLogoRequest = z.infer; diff --git a/packages/trpc/server/organisation-router/update-organisation-settings.ts b/packages/trpc/server/organisation-router/update-organisation-settings.ts index 625f79114..156e40e69 100644 --- a/packages/trpc/server/organisation-router/update-organisation-settings.ts +++ b/packages/trpc/server/organisation-router/update-organisation-settings.ts @@ -44,7 +44,6 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure // Branding related settings. brandingEnabled, - brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors, @@ -174,7 +173,6 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure // Branding related settings. brandingEnabled, - brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors: normalizedBrandingColors === null ? Prisma.DbNull : normalizedBrandingColors, diff --git a/packages/trpc/server/organisation-router/update-organisation-settings.types.ts b/packages/trpc/server/organisation-router/update-organisation-settings.types.ts index d7b7d1c0d..62caa07fc 100644 --- a/packages/trpc/server/organisation-router/update-organisation-settings.types.ts +++ b/packages/trpc/server/organisation-router/update-organisation-settings.types.ts @@ -32,7 +32,6 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({ // Branding related settings. brandingEnabled: z.boolean().optional(), - brandingLogo: z.string().optional(), brandingUrl: z.string().optional(), brandingCompanyDetails: z.string().optional(), brandingColors: ZCssVarsSchema.nullish(), diff --git a/packages/trpc/server/team-router/router.ts b/packages/trpc/server/team-router/router.ts index 26a1d9913..d05e2af13 100644 --- a/packages/trpc/server/team-router/router.ts +++ b/packages/trpc/server/team-router/router.ts @@ -25,6 +25,7 @@ import { ZUpdateTeamEmailMutationSchema, } from './schema'; import { updateTeamRoute } from './update-team'; +import { updateTeamBrandingLogoRoute } from './update-team-branding-logo'; import { updateTeamGroupRoute } from './update-team-group'; import { updateTeamMemberRoute } from './update-team-member'; import { updateTeamSettingsRoute } from './update-team-settings'; @@ -50,6 +51,7 @@ export const teamRouter = router({ }, settings: { update: updateTeamSettingsRoute, + updateBrandingLogo: updateTeamBrandingLogoRoute, }, // Old routes (to be migrated) diff --git a/packages/trpc/server/team-router/update-team-branding-logo.ts b/packages/trpc/server/team-router/update-team-branding-logo.ts new file mode 100644 index 000000000..2196bb068 --- /dev/null +++ b/packages/trpc/server/team-router/update-team-branding-logo.ts @@ -0,0 +1,69 @@ +import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; +import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { buildBrandingLogoData } from '@documenso/lib/server-only/branding/store-branding-logo'; +import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims'; +import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; +import { prisma } from '@documenso/prisma'; + +import { authenticatedProcedure } from '../trpc'; +import { + ZUpdateTeamBrandingLogoRequestSchema, + ZUpdateTeamBrandingLogoResponseSchema, +} from './update-team-branding-logo.types'; + +export const updateTeamBrandingLogoRoute = authenticatedProcedure + .input(ZUpdateTeamBrandingLogoRequestSchema) + .output(ZUpdateTeamBrandingLogoResponseSchema) + .mutation(async ({ ctx, input }) => { + const { user } = ctx; + const { payload, brandingLogo } = input; + const { teamId } = payload; + + ctx.logger.info({ + input: { + teamId, + }, + }); + + const team = await prisma.team.findFirst({ + where: buildTeamWhereQuery({ + teamId, + userId: user.id, + roles: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'], + }), + }); + + if (!team) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You do not have permission to update this team.', + }); + } + + // Setting a logo requires the custom-branding entitlement; clearing it is + // always allowed so a downgraded team can still remove its logo. + if (brandingLogo && IS_BILLING_ENABLED()) { + const claim = await getOrganisationClaimByTeamId({ teamId }); + + if (claim.flags?.allowCustomBranding !== true) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Your plan does not allow custom branding.', + }); + } + } + + const brandingLogoValue = brandingLogo ? await buildBrandingLogoData(brandingLogo) : ''; + + await prisma.team.update({ + where: { + id: team.id, + }, + data: { + teamGlobalSettings: { + update: { + brandingLogo: brandingLogoValue, + }, + }, + }, + }); + }); diff --git a/packages/trpc/server/team-router/update-team-branding-logo.types.ts b/packages/trpc/server/team-router/update-team-branding-logo.types.ts new file mode 100644 index 000000000..171537eaa --- /dev/null +++ b/packages/trpc/server/team-router/update-team-branding-logo.types.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { zfd } from 'zod-form-data'; + +import { zfdBrandingImageFile, zodFormData } from '../../utils/zod-form-data'; + +export const ZUpdateTeamBrandingLogoRequestSchema = zodFormData({ + payload: zfd.json( + z.object({ + teamId: z.number(), + }), + ), + brandingLogo: zfdBrandingImageFile().optional(), +}); + +export const ZUpdateTeamBrandingLogoResponseSchema = z.void(); + +export type TUpdateTeamBrandingLogoRequest = z.infer; diff --git a/packages/trpc/server/team-router/update-team-settings.ts b/packages/trpc/server/team-router/update-team-settings.ts index c5db1d12a..c8a81b4b8 100644 --- a/packages/trpc/server/team-router/update-team-settings.ts +++ b/packages/trpc/server/team-router/update-team-settings.ts @@ -42,7 +42,6 @@ export const updateTeamSettingsRoute = authenticatedProcedure // Branding related settings. brandingEnabled, - brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors, @@ -176,7 +175,6 @@ export const updateTeamSettingsRoute = authenticatedProcedure // Branding related settings. brandingEnabled, - brandingLogo, brandingUrl, brandingCompanyDetails, brandingColors: normalizedBrandingColors === null ? Prisma.DbNull : normalizedBrandingColors, diff --git a/packages/trpc/server/team-router/update-team-settings.types.ts b/packages/trpc/server/team-router/update-team-settings.types.ts index 31ec4c5b0..72990ce77 100644 --- a/packages/trpc/server/team-router/update-team-settings.types.ts +++ b/packages/trpc/server/team-router/update-team-settings.types.ts @@ -35,7 +35,6 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({ // Branding related settings. brandingEnabled: z.boolean().nullish(), - brandingLogo: z.string().nullish(), brandingUrl: z.string().nullish(), brandingCompanyDetails: z.string().nullish(), brandingColors: ZCssVarsSchema.nullish(), diff --git a/packages/trpc/utils/zod-form-data.ts b/packages/trpc/utils/zod-form-data.ts index d62c50635..82f3b39d2 100644 --- a/packages/trpc/utils/zod-form-data.ts +++ b/packages/trpc/utils/zod-form-data.ts @@ -1,4 +1,9 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; +import { + BRANDING_LOGO_ALLOWED_TYPES, + BRANDING_LOGO_MAX_SIZE_BYTES, + BRANDING_LOGO_MAX_SIZE_MB, +} from '@documenso/lib/constants/branding'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import type { ZodRawShape } from 'zod'; import z from 'zod'; @@ -17,6 +22,21 @@ export const zfdFile = () => { }); }; +/** + * A `zfd.file()` schema constrained to branding-logo images: size-limited and + * restricted to a MIME allowlist. Use for server-side branding logo uploads. + */ +export const zfdBrandingImageFile = () => { + return zfd + .file() + .refine((file) => file.size <= BRANDING_LOGO_MAX_SIZE_BYTES, { + message: `File cannot be larger than ${BRANDING_LOGO_MAX_SIZE_MB}MB`, + }) + .refine((file) => BRANDING_LOGO_ALLOWED_TYPES.includes(file.type), { + message: 'File must be a JPG, PNG, or WebP image', + }); +}; + /** * This helper takes the place of the `z.object` at the root of your schema. * It wraps your schema in a `z.preprocess` that extracts all the data out of a `FormData` From a55e6d94849a3a127f20f160fe93d466df7c9c13 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Thu, 2 Jul 2026 09:20:56 +0300 Subject: [PATCH 23/41] fix: block invisible & control characters and URLs in names (#2978) --- .../dialogs/folder-create-dialog.tsx | 5 +- .../dialogs/folder-update-dialog.tsx | 6 +- .../dialogs/passkey-create-dialog.tsx | 4 +- .../dialogs/team-email-update-dialog.tsx | 10 +- .../components/forms/email-transport-form.tsx | 5 +- apps/remix/app/components/forms/profile.tsx | 2 +- apps/remix/app/components/forms/signup.tsx | 2 +- .../app/components/general/claim-account.tsx | 4 +- ...ettings-security-passkey-table-actions.tsx | 3 +- .../o.$orgUrl.settings.groups.$id.tsx | 5 +- packages/auth/server/types/email-password.ts | 2 +- packages/lib/constants/auth.ts | 15 -- packages/lib/types/name.test.ts | 145 ++++++++++++++++++ packages/lib/types/name.ts | 68 ++++++++ .../create-admin-organisation.types.ts | 5 +- .../create-subscription-claim.types.ts | 3 +- .../server/admin-router/create-user.types.ts | 2 +- .../create-email-transport.types.ts | 5 +- .../update-email-transport.types.ts | 5 +- .../update-admin-organisation.types.ts | 4 +- .../server/admin-router/update-user.types.ts | 3 +- .../create-api-token.types.ts | 3 +- .../auth-router/create-passkey.types.ts | 3 +- .../auth-router/update-passkey.types.ts | 3 +- .../create-organisation-email.types.ts | 3 +- packages/trpc/server/folder-router/schema.ts | 5 +- .../create-organisation-group.types.ts | 3 +- .../create-organisation.types.ts | 8 +- .../update-organisation-group.types.ts | 3 +- packages/trpc/server/profile-router/schema.ts | 2 +- .../server/team-router/create-team.types.ts | 5 +- packages/trpc/server/team-router/schema.ts | 11 +- .../server/team-router/update-team.types.ts | 5 +- packages/trpc/server/webhook-router/schema.ts | 8 + 34 files changed, 286 insertions(+), 79 deletions(-) create mode 100644 packages/lib/types/name.test.ts create mode 100644 packages/lib/types/name.ts diff --git a/apps/remix/app/components/dialogs/folder-create-dialog.tsx b/apps/remix/app/components/dialogs/folder-create-dialog.tsx index c2cf21eaa..395feaf37 100644 --- a/apps/remix/app/components/dialogs/folder-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/folder-create-dialog.tsx @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -23,7 +24,7 @@ import { useParams } from 'react-router'; import { z } from 'zod'; const ZCreateFolderFormSchema = z.object({ - name: z.string().min(1, { message: 'Folder name is required' }), + name: ZNameSchema, }); type TCreateFolderFormSchema = z.infer; @@ -65,7 +66,7 @@ export const FolderCreateDialog = ({ type, trigger, parentFolderId, ...props }: toast({ description: t`Folder created successfully`, }); - } catch (err) { + } catch (_err) { toast({ title: t`Failed to create folder`, description: t`An unknown error occurred while creating the folder.`, diff --git a/apps/remix/app/components/dialogs/folder-update-dialog.tsx b/apps/remix/app/components/dialogs/folder-update-dialog.tsx index 1c57bc27d..db859431a 100644 --- a/apps/remix/app/components/dialogs/folder-update-dialog.tsx +++ b/apps/remix/app/components/dialogs/folder-update-dialog.tsx @@ -1,5 +1,6 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema'; import { Button } from '@documenso/ui/primitives/button'; @@ -23,8 +24,6 @@ import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { useOptionalCurrentTeam } from '~/providers/team'; - export type FolderUpdateDialogProps = { folder: TFolderWithSubfolders | null; isOpen: boolean; @@ -32,7 +31,7 @@ export type FolderUpdateDialogProps = { } & Omit; export const ZUpdateFolderFormSchema = z.object({ - name: z.string().min(1), + name: ZNameSchema, visibility: z.nativeEnum(DocumentVisibility).optional(), }); @@ -40,7 +39,6 @@ export type TUpdateFolderFormSchema = z.infer; export const FolderUpdateDialog = ({ folder, isOpen, onOpenChange }: FolderUpdateDialogProps) => { const { t } = useLingui(); - const team = useOptionalCurrentTeam(); const { toast } = useToast(); const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation(); diff --git a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx index 5a51cd8d4..3fe62a653 100644 --- a/apps/remix/app/components/dialogs/passkey-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/passkey-create-dialog.tsx @@ -1,5 +1,6 @@ import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; import { AppError } from '@documenso/lib/errors/app-error'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Button } from '@documenso/ui/primitives/button'; @@ -25,14 +26,13 @@ import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; import { UAParser } from 'ua-parser-js'; import { z } from 'zod'; - export type PasskeyCreateDialogProps = { trigger?: React.ReactNode; onSuccess?: () => void; } & Omit; const ZCreatePasskeyFormSchema = z.object({ - passkeyName: z.string().min(3), + passkeyName: ZNameSchema, }); type TCreatePasskeyFormSchema = z.infer; diff --git a/apps/remix/app/components/dialogs/team-email-update-dialog.tsx b/apps/remix/app/components/dialogs/team-email-update-dialog.tsx index 6eb5b0e88..3fbddc3c2 100644 --- a/apps/remix/app/components/dialogs/team-email-update-dialog.tsx +++ b/apps/remix/app/components/dialogs/team-email-update-dialog.tsx @@ -1,4 +1,5 @@ import { trpc } from '@documenso/trpc/react'; +import { ZUpdateTeamEmailMutationSchema } from '@documenso/trpc/server/team-router/schema'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -19,16 +20,16 @@ import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { useRevalidator } from 'react-router'; -import { z } from 'zod'; +import type { z } from 'zod'; export type TeamEmailUpdateDialogProps = { teamEmail: TeamEmail; trigger?: React.ReactNode; } & Omit; -const ZUpdateTeamEmailFormSchema = z.object({ - name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), -}); +const ZUpdateTeamEmailFormSchema = ZUpdateTeamEmailMutationSchema.pick({ + data: true, +}).shape.data; type TUpdateTeamEmailFormSchema = z.infer; @@ -44,6 +45,7 @@ export const TeamEmailUpdateDialog = ({ teamEmail, trigger, ...props }: TeamEmai defaultValues: { name: teamEmail.name, }, + mode: 'onSubmit', }); const { mutateAsync: updateTeamEmail } = trpc.team.email.update.useMutation(); diff --git a/apps/remix/app/components/forms/email-transport-form.tsx b/apps/remix/app/components/forms/email-transport-form.tsx index 2e20fdb59..c8af2f478 100644 --- a/apps/remix/app/components/forms/email-transport-form.tsx +++ b/apps/remix/app/components/forms/email-transport-form.tsx @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { Form, FormControl, @@ -15,8 +16,8 @@ import { useForm } from 'react-hook-form'; import { z } from 'zod'; const ZEmailTransportFormSchema = z.object({ - name: z.string().min(1), - fromName: z.string().min(1), + name: ZNameSchema, + fromName: ZNameSchema, fromAddress: z.string().email(), type: z.enum(['SMTP_AUTH', 'SMTP_API', 'RESEND', 'MAILCHANNELS']), host: z.string().optional(), diff --git a/apps/remix/app/components/forms/profile.tsx b/apps/remix/app/components/forms/profile.tsx index a9b81bc02..3449a747f 100644 --- a/apps/remix/app/components/forms/profile.tsx +++ b/apps/remix/app/components/forms/profile.tsx @@ -1,5 +1,5 @@ import { useSession } from '@documenso/lib/client-only/providers/session'; -import { ZNameSchema } from '@documenso/lib/constants/auth'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/apps/remix/app/components/forms/signup.tsx b/apps/remix/app/components/forms/signup.tsx index a53131f08..4c7a18aec 100644 --- a/apps/remix/app/components/forms/signup.tsx +++ b/apps/remix/app/components/forms/signup.tsx @@ -1,8 +1,8 @@ import communityCardsImage from '@documenso/assets/images/community-cards.png'; import { authClient } from '@documenso/auth/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; -import { ZNameSchema } from '@documenso/lib/constants/auth'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { env } from '@documenso/lib/utils/env'; import { zEmail } from '@documenso/lib/utils/zod'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; diff --git a/apps/remix/app/components/general/claim-account.tsx b/apps/remix/app/components/general/claim-account.tsx index 110549bc7..4301391e6 100644 --- a/apps/remix/app/components/general/claim-account.tsx +++ b/apps/remix/app/components/general/claim-account.tsx @@ -1,6 +1,7 @@ import { authClient } from '@documenso/auth/client'; import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; import { AppError } from '@documenso/lib/errors/app-error'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { env } from '@documenso/lib/utils/env'; import { zEmail } from '@documenso/lib/utils/zod'; import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema'; @@ -19,7 +20,6 @@ import { useRef } from 'react'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import { z } from 'zod'; - import { SIGNUP_ERROR_MESSAGES } from '~/components/forms/signup'; export type ClaimAccountProps = { @@ -30,7 +30,7 @@ export type ClaimAccountProps = { export const ZClaimAccountFormSchema = z .object({ - name: z.string().trim().min(1, { message: msg`Please enter a valid name.`.id }), + name: ZNameSchema, email: zEmail().min(1), password: ZPasswordSchema, }) diff --git a/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx index 7feb0e191..180dc3e44 100644 --- a/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx +++ b/apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -29,7 +30,7 @@ export type SettingsSecurityPasskeyTableActionsProps = { }; const ZUpdatePasskeySchema = z.object({ - name: z.string(), + name: ZNameSchema, }); type TUpdatePasskeySchema = z.infer; diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx index 307cfe479..b3abcb81a 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx @@ -3,6 +3,7 @@ import { ORGANISATION_MEMBER_ROLE_HIERARCHY } from '@documenso/lib/constants/org import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations'; import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams-translations'; import { AppError } from '@documenso/lib/errors/app-error'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { trpc } from '@documenso/trpc/react'; import type { TFindOrganisationGroupsResponse } from '@documenso/trpc/server/organisation-router/find-organisation-groups.types'; import { Button } from '@documenso/ui/primitives/button'; @@ -28,7 +29,6 @@ import { useMemo, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Link } from 'react-router'; import { z } from 'zod'; - import { OrganisationGroupDeleteDialog } from '~/components/dialogs/organisation-group-delete-dialog'; import { GenericErrorLayout } from '~/components/general/generic-error-layout'; import { @@ -36,7 +36,6 @@ import { OrganisationMembersMultiSelectCombobox, } from '~/components/general/organisation-members-multiselect-combobox'; import { SettingsHeader } from '~/components/general/settings-header'; - import type { Route } from './+types/o.$orgUrl.settings.groups.$id'; export default function OrganisationGroupSettingsPage({ params }: Route.ComponentProps) { @@ -113,7 +112,7 @@ export default function OrganisationGroupSettingsPage({ params }: Route.Componen } const ZUpdateOrganisationGroupFormSchema = z.object({ - name: z.string().min(1, msg`Name is required`.id), + name: ZNameSchema, organisationRole: z.nativeEnum(OrganisationMemberRole), memberIds: z.array(z.string()), }); diff --git a/packages/auth/server/types/email-password.ts b/packages/auth/server/types/email-password.ts index eafaca28c..5303d326c 100644 --- a/packages/auth/server/types/email-password.ts +++ b/packages/auth/server/types/email-password.ts @@ -1,4 +1,4 @@ -import { ZNameSchema } from '@documenso/lib/constants/auth'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { zEmail } from '@documenso/lib/utils/zod'; import { z } from 'zod'; diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index c4c8727ed..cb514979b 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -1,25 +1,10 @@ import MailChecker from 'mailchecker'; -import { z } from 'zod'; import { env } from '../utils/env'; import { NEXT_PUBLIC_WEBAPP_URL } from './app'; export const SALT_ROUNDS = 12; -export const URL_PATTERN = /https?:\/\/|www\./i; - -/** - * Shared name schema that disallows URLs to prevent phishing via email rendering. - */ -export const ZNameSchema = z - .string() - .trim() - .min(3, { message: 'Please enter a valid name.' }) - .max(255, { message: 'Name cannot be more than 255 characters.' }) - .refine((value) => !URL_PATTERN.test(value), { - message: 'Name cannot contain URLs.', - }); - export const IDENTITY_PROVIDER_NAME: Record = { DOCUMENSO: 'Documenso', GOOGLE: 'Google', diff --git a/packages/lib/types/name.test.ts b/packages/lib/types/name.test.ts new file mode 100644 index 000000000..376831883 --- /dev/null +++ b/packages/lib/types/name.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest'; + +import { ZNameSchema } from './name'; + +describe('ZNameSchema', () => { + describe('valid names', () => { + it('accepts a normal name', () => { + expect(ZNameSchema.safeParse('Example User')).toEqual({ + success: true, + data: 'Example User', + }); + }); + + it('accepts international characters', () => { + expect(ZNameSchema.safeParse('Døcumensø Üser')).toEqual({ + success: true, + data: 'Døcumensø Üser', + }); + }); + + it('trims surrounding whitespace', () => { + expect(ZNameSchema.safeParse(' Documenso User ')).toEqual({ + success: true, + data: 'Documenso User', + }); + }); + + it('accepts names at the minimum length', () => { + expect(ZNameSchema.safeParse('DU')).toEqual({ + success: true, + data: 'DU', + }); + }); + + it('accepts names at the maximum length', () => { + const name = + 'DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser Do'; + + expect(name.length).toBe(100); + expect(ZNameSchema.safeParse(name)).toEqual({ + success: true, + data: name, + }); + }); + }); + + describe('length validation', () => { + it('rejects names shorter than 2 characters', () => { + expect(ZNameSchema.safeParse('D')).toMatchObject({ + success: false, + error: { + issues: [{ message: 'Please enter a valid name.' }], + }, + }); + }); + + it('rejects names longer than 100 characters', () => { + const name = + 'DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser DocumensoUser Doc'; + + expect(name.length).toBe(101); + expect(ZNameSchema.safeParse(name)).toMatchObject({ + success: false, + error: { + issues: [{ message: 'Name cannot be more than 100 characters.' }], + }, + }); + }); + + it('rejects whitespace-only input after trim', () => { + expect(ZNameSchema.safeParse(' ')).toMatchObject({ + success: false, + }); + }); + }); + + describe('URL validation', () => { + it.each([ + 'https://example.com', + 'http://example.com', + 'HTTPS://EXAMPLE.COM', + 'Northwind www.example.com', + 'www.example.com', + ])('rejects URLs in names: %s', (value) => { + expect(ZNameSchema.safeParse(value)).toMatchObject({ + success: false, + error: { + issues: expect.arrayContaining([expect.objectContaining({ message: 'Name cannot contain URLs.' })]), + }, + }); + }); + }); + + describe('invalid character validation', () => { + it.each([ + ['NUL character', 'Acme\u0000Corp'], + ['zero-width space', 'Acme\u200bCorp'], + ['bidi override', 'Acme\u202eCorp'], + ['byte order mark', 'Acme\ufeffCorp'], + ['lone surrogate', 'Acme\ud800Corp'], + ['tag character', `Acme${String.fromCodePoint(0xe0041)}Corp`], + ['noncharacter', 'Acme\ufffeCorp'], + ['private use character', 'Acme\ue000Corp'], + ['Hangul filler', 'Acme\u3164Corp'], + ['braille blank', 'Acme\u2800Corp'], + ['combining grapheme joiner', 'Acme\u034fCorp'], + ])('rejects names containing a %s', (_label, value) => { + expect(ZNameSchema.safeParse(value)).toMatchObject({ + success: false, + error: { + issues: expect.arrayContaining([expect.objectContaining({ message: 'Name contains invalid characters.' })]), + }, + }); + }); + + it.each([ + ['fixed form', String.raw`Acme\u200bCorp`], + ['uppercase U', String.raw`Acme\U200BCorp`], + ['braced form', String.raw`Acme\u{200b}Corp`], + ['braced form with leading zeros', String.raw`Acme\u{0000200b}Corp`], + ['lone surrogate', String.raw`Acme\ud800Corp`], + ])('rejects literal \\u escape sequences stored as text (%s)', (_label, value) => { + expect(ZNameSchema.safeParse(value)).toMatchObject({ + success: false, + error: { + issues: expect.arrayContaining([expect.objectContaining({ message: 'Name contains invalid characters.' })]), + }, + }); + }); + + it.each([ + ['escape of a valid code point', String.raw`Acme\u0041Corp`], + ['braced escape of a valid astral code point', String.raw`Acme\u{1F600}Corp`], + ['braced escape beyond the Unicode range', String.raw`Acme\u{FFFFFFF}Corp`], + ['incomplete escape sequence', String.raw`Acme\u00 Corp`], + ['unterminated braced escape', String.raw`Acme\u{200bCorp`], + ['astral characters such as emoji', 'Acme 😀 Corp'], + ['emoji with a variation selector', 'I ❤️ Docs'], + ])('accepts %s', (_label, value) => { + expect(ZNameSchema.safeParse(value)).toMatchObject({ + success: true, + }); + }); + }); +}); diff --git a/packages/lib/types/name.ts b/packages/lib/types/name.ts new file mode 100644 index 000000000..14f0f4b7d --- /dev/null +++ b/packages/lib/types/name.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +export const URL_PATTERN = /https?:\/\/|www\./i; + +/** + * Characters that render as empty/invisible or break text layout: + * + * - `\p{C}` - control, format, lone surrogate, private use and + * unassigned code points (NUL, zero-width spaces, bidi + * overrides, BOM, tag characters, noncharacters). + * - `\p{Zl}\p{Zp}` - line and paragraph separators. + * - `\u{034F}` - combining grapheme joiner (invisible). Kept outside the + * character class because it is a combining mark, which + * lint rules reject inside classes. + * - remaining - letters that render as blank (Hangul fillers, braille blank). + * + * The `\p{...}` classes are maintained by the Unicode database, so newly + * assigned characters in these categories are covered automatically. + */ +const INVALID_CHARACTER_REGEX = /[\p{C}\p{Zl}\p{Zp}\u{115F}\u{1160}\u{2800}\u{3164}\u{FFA0}]|\u{034F}/u; + +const hasInvalidCharacter = (value: string) => INVALID_CHARACTER_REGEX.test(value); + +/** + * Matches literal `\uXXXX` and `\u{XXXX}` escape sequences stored verbatim as + * text (e.g. the 6 characters `\`, `u`, `2`, `0`, `0`, `b`), which can still + * break rendering downstream if anything decodes them. + */ +const ESCAPE_SEQUENCE_PATTERN = /\\u(?:([0-9a-f]{4})|\{([0-9a-f]+)\})/gi; + +const hasInvalidEscapeSequence = (value: string) => { + for (const [, fixedHex, bracedHex] of value.matchAll(ESCAPE_SEQUENCE_PATTERN)) { + const codePoint = parseInt(fixedHex ?? bracedHex, 16); + + if (codePoint > 0x10ffff) { + continue; + } + + // Decode the escape and run it through the same character policy as the + // unescaped check, so the two can never drift apart. + if (hasInvalidCharacter(String.fromCodePoint(codePoint))) { + return true; + } + } + + return false; +}; + +export const hasInvalidTextCharacters = (value: string) => + hasInvalidCharacter(value) || hasInvalidEscapeSequence(value); + +/** + * Shared name schema that disallows URLs to prevent phishing via email rendering, + * and invisible/control characters that render as empty or break the UI. + */ +export const ZNameSchema = z + .string() + .trim() + .min(2, { message: 'Please enter a valid name.' }) + .max(100, { message: 'Name cannot be more than 100 characters.' }) + .refine((value) => !URL_PATTERN.test(value), { + message: 'Name cannot contain URLs.', + }) + .refine((value) => !hasInvalidTextCharacters(value), { + message: 'Name contains invalid characters.', + }); + +export type TName = z.infer; diff --git a/packages/trpc/server/admin-router/create-admin-organisation.types.ts b/packages/trpc/server/admin-router/create-admin-organisation.types.ts index 7c142bba6..a86750ab3 100644 --- a/packages/trpc/server/admin-router/create-admin-organisation.types.ts +++ b/packages/trpc/server/admin-router/create-admin-organisation.types.ts @@ -1,11 +1,10 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; -import { ZOrganisationNameSchema } from '../organisation-router/create-organisation.types'; - export const ZCreateAdminOrganisationRequestSchema = z.object({ ownerUserId: z.number(), data: z.object({ - name: ZOrganisationNameSchema, + name: ZNameSchema, }), }); diff --git a/packages/trpc/server/admin-router/create-subscription-claim.types.ts b/packages/trpc/server/admin-router/create-subscription-claim.types.ts index f1cfbad60..9cb1da986 100644 --- a/packages/trpc/server/admin-router/create-subscription-claim.types.ts +++ b/packages/trpc/server/admin-router/create-subscription-claim.types.ts @@ -1,8 +1,9 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { ZClaimFlagsSchema, ZRateLimitArraySchema } from '@documenso/lib/types/subscription'; import { z } from 'zod'; export const ZCreateSubscriptionClaimRequestSchema = z.object({ - name: z.string().min(1), + name: ZNameSchema, teamCount: z.number().int().min(0), memberCount: z.number().int().min(0), envelopeItemCount: z.number().int().min(1), diff --git a/packages/trpc/server/admin-router/create-user.types.ts b/packages/trpc/server/admin-router/create-user.types.ts index 6e1b65438..6c629a291 100644 --- a/packages/trpc/server/admin-router/create-user.types.ts +++ b/packages/trpc/server/admin-router/create-user.types.ts @@ -1,4 +1,4 @@ -import { ZNameSchema } from '@documenso/lib/constants/auth'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; export const ZCreateUserRequestSchema = z.object({ diff --git a/packages/trpc/server/admin-router/email-transport/create-email-transport.types.ts b/packages/trpc/server/admin-router/email-transport/create-email-transport.types.ts index 26a47c12c..ecfa77bd4 100644 --- a/packages/trpc/server/admin-router/email-transport/create-email-transport.types.ts +++ b/packages/trpc/server/admin-router/email-transport/create-email-transport.types.ts @@ -1,9 +1,10 @@ import { ZEmailTransportConfigSchema } from '@documenso/lib/server-only/email/email-transport-config'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; export const ZCreateEmailTransportRequestSchema = z.object({ - name: z.string().min(1), - fromName: z.string().min(1), + name: ZNameSchema, + fromName: ZNameSchema, fromAddress: z.string().email(), config: ZEmailTransportConfigSchema, }); diff --git a/packages/trpc/server/admin-router/email-transport/update-email-transport.types.ts b/packages/trpc/server/admin-router/email-transport/update-email-transport.types.ts index d007c7e19..c8924c175 100644 --- a/packages/trpc/server/admin-router/email-transport/update-email-transport.types.ts +++ b/packages/trpc/server/admin-router/email-transport/update-email-transport.types.ts @@ -4,6 +4,7 @@ import { ZSmtpApiConfigSchema, ZSmtpAuthConfigSchema, } from '@documenso/lib/server-only/email/email-transport-config'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; // Reuses the canonical transport config schemas, but relaxes the secret field so @@ -21,8 +22,8 @@ const ZUpdateConfigSchema = z.discriminatedUnion('type', [ export const ZUpdateEmailTransportRequestSchema = z.object({ id: z.string(), data: z.object({ - name: z.string().min(1), - fromName: z.string().min(1), + name: ZNameSchema, + fromName: ZNameSchema, fromAddress: z.string().email(), config: ZUpdateConfigSchema, }), diff --git a/packages/trpc/server/admin-router/update-admin-organisation.types.ts b/packages/trpc/server/admin-router/update-admin-organisation.types.ts index 5f5d4a17a..47e9927cc 100644 --- a/packages/trpc/server/admin-router/update-admin-organisation.types.ts +++ b/packages/trpc/server/admin-router/update-admin-organisation.types.ts @@ -1,13 +1,13 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; -import { ZOrganisationNameSchema } from '../organisation-router/create-organisation.types'; import { ZTeamUrlSchema } from '../team-router/schema'; import { ZCreateSubscriptionClaimRequestSchema } from './create-subscription-claim.types'; export const ZUpdateAdminOrganisationRequestSchema = z.object({ organisationId: z.string(), data: z.object({ - name: ZOrganisationNameSchema.optional(), + name: ZNameSchema.optional(), url: ZTeamUrlSchema.optional(), claims: ZCreateSubscriptionClaimRequestSchema.pick({ teamCount: true, diff --git a/packages/trpc/server/admin-router/update-user.types.ts b/packages/trpc/server/admin-router/update-user.types.ts index 153b5a785..300db6fa7 100644 --- a/packages/trpc/server/admin-router/update-user.types.ts +++ b/packages/trpc/server/admin-router/update-user.types.ts @@ -1,10 +1,11 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { zEmail } from '@documenso/lib/utils/zod'; import { Role } from '@prisma/client'; import { z } from 'zod'; export const ZUpdateUserRequestSchema = z.object({ id: z.number().min(1), - name: z.string().nullish(), + name: ZNameSchema.nullish(), email: zEmail().optional(), roles: z.array(z.nativeEnum(Role)).optional(), }); diff --git a/packages/trpc/server/api-token-router/create-api-token.types.ts b/packages/trpc/server/api-token-router/create-api-token.types.ts index c73c65833..c362bdf50 100644 --- a/packages/trpc/server/api-token-router/create-api-token.types.ts +++ b/packages/trpc/server/api-token-router/create-api-token.types.ts @@ -1,8 +1,9 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; export const ZCreateApiTokenRequestSchema = z.object({ teamId: z.number(), - tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }), + tokenName: ZNameSchema, expirationDate: z.string().nullable(), }); diff --git a/packages/trpc/server/auth-router/create-passkey.types.ts b/packages/trpc/server/auth-router/create-passkey.types.ts index d5d2483a4..b6ff4a163 100644 --- a/packages/trpc/server/auth-router/create-passkey.types.ts +++ b/packages/trpc/server/auth-router/create-passkey.types.ts @@ -1,8 +1,9 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn'; import { z } from 'zod'; export const ZCreatePasskeyRequestSchema = z.object({ - passkeyName: z.string().trim().min(1), + passkeyName: ZNameSchema, verificationResponse: ZRegistrationResponseJSONSchema, }); diff --git a/packages/trpc/server/auth-router/update-passkey.types.ts b/packages/trpc/server/auth-router/update-passkey.types.ts index e898234da..310389396 100644 --- a/packages/trpc/server/auth-router/update-passkey.types.ts +++ b/packages/trpc/server/auth-router/update-passkey.types.ts @@ -1,8 +1,9 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; export const ZUpdatePasskeyRequestSchema = z.object({ passkeyId: z.string().trim().min(1), - name: z.string().trim().min(1), + name: ZNameSchema, }); export const ZUpdatePasskeyResponseSchema = z.void(); diff --git a/packages/trpc/server/enterprise-router/create-organisation-email.types.ts b/packages/trpc/server/enterprise-router/create-organisation-email.types.ts index 22ff2779c..0a3830c53 100644 --- a/packages/trpc/server/enterprise-router/create-organisation-email.types.ts +++ b/packages/trpc/server/enterprise-router/create-organisation-email.types.ts @@ -1,9 +1,10 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { zEmail } from '@documenso/lib/utils/zod'; import { z } from 'zod'; export const ZCreateOrganisationEmailRequestSchema = z.object({ emailDomainId: z.string(), - emailName: z.string().min(1).max(100), + emailName: ZNameSchema, email: zEmail().toLowerCase(), // This does not need to be validated to be part of the domain. diff --git a/packages/trpc/server/folder-router/schema.ts b/packages/trpc/server/folder-router/schema.ts index a3c18b78d..396e121c1 100644 --- a/packages/trpc/server/folder-router/schema.ts +++ b/packages/trpc/server/folder-router/schema.ts @@ -1,4 +1,5 @@ import { ZFolderTypeSchema } from '@documenso/lib/types/folder-type'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params'; import { DocumentVisibility } from '@documenso/prisma/generated/types'; import FolderSchema from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; @@ -42,7 +43,7 @@ const ZFolderParentIdSchema = z .describe('The folder ID to place this folder within. Leave empty to place folder at the root level.'); export const ZCreateFolderRequestSchema = z.object({ - name: z.string(), + name: ZNameSchema, parentId: ZFolderParentIdSchema.optional(), type: ZFolderTypeSchema.optional(), }); @@ -52,7 +53,7 @@ export const ZCreateFolderResponseSchema = ZFolderSchema; export const ZUpdateFolderRequestSchema = z.object({ folderId: z.string().describe('The ID of the folder to update'), data: z.object({ - name: z.string().optional().describe('The name of the folder'), + name: ZNameSchema.optional().describe('The name of the folder'), parentId: ZFolderParentIdSchema.optional().nullable(), visibility: z.nativeEnum(DocumentVisibility).optional().describe('The visibility of the folder'), pinned: z.boolean().optional().describe('Whether the folder should be pinned'), diff --git a/packages/trpc/server/organisation-router/create-organisation-group.types.ts b/packages/trpc/server/organisation-router/create-organisation-group.types.ts index af824d426..3588f8015 100644 --- a/packages/trpc/server/organisation-router/create-organisation-group.types.ts +++ b/packages/trpc/server/organisation-router/create-organisation-group.types.ts @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { OrganisationMemberRole } from '@prisma/client'; import { z } from 'zod'; @@ -14,7 +15,7 @@ import { z } from 'zod'; export const ZCreateOrganisationGroupRequestSchema = z.object({ organisationId: z.string(), organisationRole: z.nativeEnum(OrganisationMemberRole), - name: z.string().max(100), + name: ZNameSchema, memberIds: z.array(z.string()), }); diff --git a/packages/trpc/server/organisation-router/create-organisation.types.ts b/packages/trpc/server/organisation-router/create-organisation.types.ts index e18846120..97bb6ee86 100644 --- a/packages/trpc/server/organisation-router/create-organisation.types.ts +++ b/packages/trpc/server/organisation-router/create-organisation.types.ts @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; // export const createOrganisationMeta: TrpcOpenApiMeta = { @@ -10,13 +11,8 @@ import { z } from 'zod'; // }, // }; -export const ZOrganisationNameSchema = z - .string() - .min(3, { message: 'Minimum 3 characters' }) - .max(50, { message: 'Maximum 50 characters' }); - export const ZCreateOrganisationRequestSchema = z.object({ - name: ZOrganisationNameSchema, + name: ZNameSchema, priceId: z.string().optional(), }); diff --git a/packages/trpc/server/organisation-router/update-organisation-group.types.ts b/packages/trpc/server/organisation-router/update-organisation-group.types.ts index a32187edc..e6f4eb03f 100644 --- a/packages/trpc/server/organisation-router/update-organisation-group.types.ts +++ b/packages/trpc/server/organisation-router/update-organisation-group.types.ts @@ -1,3 +1,4 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { OrganisationMemberRole } from '@prisma/client'; import { z } from 'zod'; @@ -14,7 +15,7 @@ import { z } from 'zod'; export const ZUpdateOrganisationGroupRequestSchema = z.object({ id: z.string(), - name: z.string().nullable().optional(), + name: ZNameSchema.nullable().optional(), organisationRole: z.nativeEnum(OrganisationMemberRole).optional(), memberIds: z.array(z.string()).optional(), }); diff --git a/packages/trpc/server/profile-router/schema.ts b/packages/trpc/server/profile-router/schema.ts index 4f1873096..5bcf2fe4e 100644 --- a/packages/trpc/server/profile-router/schema.ts +++ b/packages/trpc/server/profile-router/schema.ts @@ -1,4 +1,4 @@ -import { ZNameSchema } from '@documenso/lib/constants/auth'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; export const ZFindUserSecurityAuditLogsSchema = z.object({ diff --git a/packages/trpc/server/team-router/create-team.types.ts b/packages/trpc/server/team-router/create-team.types.ts index efbedccfe..b4bc56679 100644 --- a/packages/trpc/server/team-router/create-team.types.ts +++ b/packages/trpc/server/team-router/create-team.types.ts @@ -1,5 +1,6 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; -import { ZTeamNameSchema, ZTeamUrlSchema } from './schema'; +import { ZTeamUrlSchema } from './schema'; // export const createTeamMeta: TrpcOpenApiMeta = { // openapi: { @@ -13,7 +14,7 @@ import { ZTeamNameSchema, ZTeamUrlSchema } from './schema'; export const ZCreateTeamRequestSchema = z.object({ organisationId: z.string(), - teamName: ZTeamNameSchema, + teamName: ZNameSchema, teamUrl: ZTeamUrlSchema, inheritMembers: z .boolean() diff --git a/packages/trpc/server/team-router/schema.ts b/packages/trpc/server/team-router/schema.ts index 35ee0b59a..620ad1cb4 100644 --- a/packages/trpc/server/team-router/schema.ts +++ b/packages/trpc/server/team-router/schema.ts @@ -1,5 +1,5 @@ -import { URL_PATTERN, ZNameSchema } from '@documenso/lib/constants/auth'; import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams'; +import { ZNameSchema } from '@documenso/lib/types/name'; import { zEmail } from '@documenso/lib/utils/zod'; import { TeamMemberRole } from '@prisma/client'; import { z } from 'zod'; @@ -32,15 +32,6 @@ export const ZTeamUrlSchema = z message: 'This URL is already in use.', }); -export const ZTeamNameSchema = z - .string() - .trim() - .min(3, { message: 'Team name must be at least 3 characters long.' }) - .max(30, { message: 'Team name must not exceed 30 characters.' }) - .refine((value) => !URL_PATTERN.test(value), { - message: 'Team name cannot contain URLs.', - }); - export const ZCreateTeamEmailVerificationMutationSchema = z.object({ teamId: z.number(), name: ZNameSchema, diff --git a/packages/trpc/server/team-router/update-team.types.ts b/packages/trpc/server/team-router/update-team.types.ts index c75abdf30..f28675b1b 100644 --- a/packages/trpc/server/team-router/update-team.types.ts +++ b/packages/trpc/server/team-router/update-team.types.ts @@ -1,6 +1,7 @@ +import { ZNameSchema } from '@documenso/lib/types/name'; import { z } from 'zod'; -import { ZTeamNameSchema, ZTeamUrlSchema } from './schema'; +import { ZTeamUrlSchema } from './schema'; export const MAX_PROFILE_BIO_LENGTH = 256; @@ -19,7 +20,7 @@ export const MAX_PROFILE_BIO_LENGTH = 256; export const ZUpdateTeamRequestSchema = z.object({ teamId: z.number(), data: z.object({ - name: ZTeamNameSchema.optional(), + name: ZNameSchema.optional(), url: ZTeamUrlSchema.optional(), profileBio: z .string() diff --git a/packages/trpc/server/webhook-router/schema.ts b/packages/trpc/server/webhook-router/schema.ts index 1abd126a4..87ebf53d2 100644 --- a/packages/trpc/server/webhook-router/schema.ts +++ b/packages/trpc/server/webhook-router/schema.ts @@ -1,4 +1,5 @@ import { isPrivateUrl } from '@documenso/lib/server-only/webhooks/is-private-url'; +import { URL_PATTERN } from '@documenso/lib/types/name'; import { WebhookTriggerEvents } from '@prisma/client'; import { z } from 'zod'; @@ -7,6 +8,13 @@ export const ZWebhookUrlSchema = z .url() .refine((url) => !isPrivateUrl(url), { message: 'Webhook URL cannot point to a private or loopback address', + }) + /* + * Without this, values like "foo: bar" would be valid URLs. + * Keep the same error message as the zod url() validator. + */ + .refine((value) => URL_PATTERN.test(value), { + message: 'Invalid url', }); export const ZCreateWebhookRequestSchema = z.object({ From 50f272be876f14a2e22518552f5030c3117c3391 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Thu, 2 Jul 2026 09:50:11 +0300 Subject: [PATCH 24/41] fix: admin organisation limits and usage UI (#3014) --- .../general/admin-global-settings-section.tsx | 113 +++++-- .../components/general/claim-limit-fields.tsx | 95 ++++-- .../general/organisation-usage-panel.tsx | 302 +++++++++++++---- .../organisation-usage-reset-button.tsx | 2 + .../general/rate-limit-array-input.tsx | 165 +++++++-- .../admin+/organisations.$id.tsx | 312 +++++++++--------- .../_authenticated+/admin+/teams.$id.tsx | 6 +- .../rate-limit/compute-quota-flags.ts | 35 +- .../rate-limit/get-quota-alert-kind.ts | 4 +- packages/lib/types/subscription.ts | 39 ++- packages/lib/universal/quota-usage.test.ts | 99 ++++++ packages/lib/universal/quota-usage.ts | 57 ++++ .../server/admin-router/get-admin-team.ts | 1 + .../admin-router/get-admin-team.types.ts | 3 + 14 files changed, 880 insertions(+), 353 deletions(-) create mode 100644 packages/lib/universal/quota-usage.test.ts create mode 100644 packages/lib/universal/quota-usage.ts diff --git a/apps/remix/app/components/general/admin-global-settings-section.tsx b/apps/remix/app/components/general/admin-global-settings-section.tsx index 5f21e7900..760684077 100644 --- a/apps/remix/app/components/general/admin-global-settings-section.tsx +++ b/apps/remix/app/components/general/admin-global-settings-section.tsx @@ -5,6 +5,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import type { OrganisationGlobalSettings, TeamGlobalSettings } from '@prisma/client'; +import type { ReactNode } from 'react'; import { DetailsCard, DetailsValue } from '~/components/general/admin-details'; @@ -25,38 +26,72 @@ const emailSettingsKeys = Object.keys(EMAIL_SETTINGS_LABELS) as (keyof TDocument type AdminGlobalSettingsSectionProps = { settings: TeamGlobalSettings | OrganisationGlobalSettings | null; isTeam?: boolean; + /** When viewing a team, the parent organisation settings the team inherits from. */ + inheritedSettings?: OrganisationGlobalSettings | null; }; -export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGlobalSettingsSectionProps) => { +export const AdminGlobalSettingsSection = ({ + settings, + isTeam = false, + inheritedSettings, +}: AdminGlobalSettingsSectionProps) => { const { _ } = useLingui(); - const notSetLabel = isTeam ? Inherited : Not set; if (!settings) { return null; } - const textValue = (value: string | null | undefined) => { - if (value === null || value === undefined) { - return notSetLabel; + const notSet = Not set; + + const inheritedValue = (value: ReactNode) => { + if (!isTeam || value === null) { + return notSet; } - return value; + return ( + + + Inherited: + + {value} + + ); }; - const brandingTextValue = (value: string | null | undefined) => { - if (value === null || value === undefined || value.trim() === '') { - return notSetLabel; + const textValue = (value: string | null | undefined, inherited?: string | null) => { + if (value && value.trim() !== '') { + return value; } - return value; + if (inherited && inherited.trim() !== '') { + return inheritedValue(inherited); + } + + return notSet; }; - const booleanValue = (value: boolean | null | undefined) => { - if (value === null || value === undefined) { - return notSetLabel; + const booleanLabel = (value: boolean) => (value ? Enabled : Disabled); + + const booleanValue = (value: boolean | null | undefined, inherited?: boolean | null) => { + if (value !== null && value !== undefined) { + return booleanLabel(value); } - return value ? Enabled : Disabled; + return inherited !== null && inherited !== undefined ? inheritedValue(booleanLabel(inherited)) : notSet; + }; + + const visibilityLabel = (value: string | null | undefined) => { + return value && DOCUMENT_VISIBILITY[value] ? _(DOCUMENT_VISIBILITY[value].value) : null; + }; + + const visibilityValue = (value: string | null | undefined, inherited?: string | null) => { + const label = visibilityLabel(value); + + if (label !== null) { + return label; + } + + return inheritedValue(visibilityLabel(inherited)); }; const parsedEmailSettings = ZDocumentEmailSettingsSchema.safeParse(settings.emailDocumentSettings); @@ -65,70 +100,82 @@ export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGl
Document visibility}> - {settings.documentVisibility != null - ? _(DOCUMENT_VISIBILITY[settings.documentVisibility].value) - : notSetLabel} + {visibilityValue(settings.documentVisibility, inheritedSettings?.documentVisibility)} Document language}> - {textValue(settings.documentLanguage)} + {textValue(settings.documentLanguage, inheritedSettings?.documentLanguage)} Document timezone}> - {textValue(settings.documentTimezone)} + {textValue(settings.documentTimezone, inheritedSettings?.documentTimezone)} Date format}> - {textValue(settings.documentDateFormat)} + {textValue(settings.documentDateFormat, inheritedSettings?.documentDateFormat)} Include sender details}> - {booleanValue(settings.includeSenderDetails)} + + {booleanValue(settings.includeSenderDetails, inheritedSettings?.includeSenderDetails)} + Include signing certificate}> - {booleanValue(settings.includeSigningCertificate)} + + {booleanValue(settings.includeSigningCertificate, inheritedSettings?.includeSigningCertificate)} + Include audit log}> - {booleanValue(settings.includeAuditLog)} + {booleanValue(settings.includeAuditLog, inheritedSettings?.includeAuditLog)} Delegate document ownership}> - {booleanValue(settings.delegateDocumentOwnership)} + + {booleanValue(settings.delegateDocumentOwnership, inheritedSettings?.delegateDocumentOwnership)} + Typed signature}> - {booleanValue(settings.typedSignatureEnabled)} + + {booleanValue(settings.typedSignatureEnabled, inheritedSettings?.typedSignatureEnabled)} + Upload signature}> - {booleanValue(settings.uploadSignatureEnabled)} + + {booleanValue(settings.uploadSignatureEnabled, inheritedSettings?.uploadSignatureEnabled)} + Draw signature}> - {booleanValue(settings.drawSignatureEnabled)} + + {booleanValue(settings.drawSignatureEnabled, inheritedSettings?.drawSignatureEnabled)} + Branding}> - {booleanValue(settings.brandingEnabled)} + {booleanValue(settings.brandingEnabled, inheritedSettings?.brandingEnabled)} Branding logo}> - {brandingTextValue(settings.brandingLogo)} + {textValue(settings.brandingLogo, inheritedSettings?.brandingLogo)} Branding URL}> - {brandingTextValue(settings.brandingUrl)} + {textValue(settings.brandingUrl, inheritedSettings?.brandingUrl)} Branding company details}> - {brandingTextValue(settings.brandingCompanyDetails)} + + {textValue(settings.brandingCompanyDetails, inheritedSettings?.brandingCompanyDetails)} + Email reply-to}> - {textValue(settings.emailReplyTo)} + {textValue(settings.emailReplyTo, inheritedSettings?.emailReplyTo)} {isTeam && parsedEmailSettings.success && ( @@ -145,7 +192,7 @@ export const AdminGlobalSettingsSection = ({ settings, isTeam = false }: AdminGl )} AI features}> - {booleanValue(settings.aiFeaturesEnabled)} + {booleanValue(settings.aiFeaturesEnabled, inheritedSettings?.aiFeaturesEnabled)}
); diff --git a/apps/remix/app/components/general/claim-limit-fields.tsx b/apps/remix/app/components/general/claim-limit-fields.tsx index ed29c8fc3..c90a92430 100644 --- a/apps/remix/app/components/general/claim-limit-fields.tsx +++ b/apps/remix/app/components/general/claim-limit-fields.tsx @@ -1,11 +1,4 @@ -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@documenso/ui/primitives/form/form'; +import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Trans, useLingui } from '@lingui/react/macro'; import type { ReactNode } from 'react'; @@ -13,6 +6,13 @@ import type { Control, FieldValues, Path } from 'react-hook-form'; import { RateLimitArrayInput } from './rate-limit-array-input'; +/** + * The rate-limit editor renders its own per-row inline errors, but a submit + * attempt can still surface array-level Zod issues (e.g. a committed duplicate + * window). Rendering the field's message here guarantees the form never fails + * silently when those errors are not tied to a row the editor is showing. + */ + type ClaimLimitFieldsProps = { control: Control; /** e.g. '' for the claim form, 'claims.' for the org admin form. */ @@ -20,6 +20,12 @@ type ClaimLimitFieldsProps = { disabled?: boolean; }; +type LimitGroup = { + title: ReactNode; + quotaKey: string; + rateLimitKey: string; +}; + export const ClaimLimitFields = ({ control, prefix = '', @@ -30,13 +36,33 @@ export const ClaimLimitFields = ({ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const name = (key: string) => `${prefix}${key}` as Path; - const renderQuotaField = (key: string, label: ReactNode, description: ReactNode) => ( + const limitGroups: LimitGroup[] = [ + { + title: Documents, + quotaKey: 'documentQuota', + rateLimitKey: 'documentRateLimits', + }, + { + title: Emails, + quotaKey: 'emailQuota', + rateLimitKey: 'emailRateLimits', + }, + { + title: API, + quotaKey: 'apiQuota', + rateLimitKey: 'apiRateLimits', + }, + ]; + + const renderQuotaField = (group: LimitGroup) => ( ( - {label} + + Monthly quota + ({ onChange={(e) => field.onChange(e.target.value === '' ? null : parseInt(e.target.value, 10))} /> - {description} )} /> ); - const renderRateLimitField = (key: string, label: ReactNode) => ( + const renderRateLimitField = (group: LimitGroup) => ( ( - {label} @@ -71,27 +95,30 @@ export const ClaimLimitFields = ({ ); return ( -
- - Limits - +
+
+

+ Limits +

+

+ + Empty quota means unlimited, 0 blocks the resource. Rate limit windows accept values like 5m, 1h or 24h. + +

+
- {renderQuotaField( - 'documentQuota', - Monthly document quota, - Empty = Unlimited, 0 = Blocked, - )} - {renderRateLimitField('documentRateLimits', Document rate limits)} +
+
+ {limitGroups.map((group) => ( +
+

{group.title}

- {renderQuotaField( - 'emailQuota', - Monthly email quota, - Empty = Unlimited, 0 = Blocked, - )} - {renderRateLimitField('emailRateLimits', Email rate limits)} - - {renderQuotaField('apiQuota', Monthly API quota, Empty = Unlimited, 0 = Blocked)} - {renderRateLimitField('apiRateLimits', API rate limits)} + {renderQuotaField(group)} + {renderRateLimitField(group)} +
+ ))} +
+
); }; diff --git a/apps/remix/app/components/general/organisation-usage-panel.tsx b/apps/remix/app/components/general/organisation-usage-panel.tsx index c9ed06265..99a69a661 100644 --- a/apps/remix/app/components/general/organisation-usage-panel.tsx +++ b/apps/remix/app/components/general/organisation-usage-panel.tsx @@ -1,13 +1,38 @@ import { currentMonthlyPeriod } from '@documenso/lib/universal/monthly-period'; +import { + getQuotaUsagePercent, + isQuotaExceeded, + isQuotaNearing, + normalizeCapacityLimit, +} from '@documenso/lib/universal/quota-usage'; +import { cn } from '@documenso/ui/lib/utils'; +import type { BadgeProps } from '@documenso/ui/primitives/badge'; +import { Badge } from '@documenso/ui/primitives/badge'; import { Progress } from '@documenso/ui/primitives/progress'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; import { Trans } from '@lingui/react/macro'; import type { OrganisationClaim, OrganisationMonthlyStat } from '@prisma/client'; -import { useState } from 'react'; -import { match } from 'ts-pattern'; +import type { LucideIcon } from 'lucide-react'; +import { FileIcon, MailIcon, MailOpenIcon, PlugIcon, UsersIcon, UsersRoundIcon } from 'lucide-react'; +import type { ReactNode } from 'react'; +import { useId, useState } from 'react'; + import { OrganisationUsageResetButton } from './organisation-usage-reset-button'; +type CapacityUsage = { + members: number; + teams: number; +}; + +type UsageRow = { + counter: 'document' | 'email' | 'api'; + label: ReactNode; + icon: LucideIcon; + used: number; + effectiveLimit: number | null; +}; + type OrganisationUsagePanelProps = { organisationId: string; monthlyStats: Pick< @@ -15,13 +40,151 @@ type OrganisationUsagePanelProps = { 'period' | 'documentCount' | 'emailCount' | 'apiCount' | 'emailReports' >[]; organisationClaim: OrganisationClaim; + capacityUsage?: CapacityUsage; +}; + +type UsageCardState = { + status: { + label: ReactNode; + variant: NonNullable; + }; + percent: number; + hasFiniteLimit: boolean; + progressClassName: string; + subtext: ReactNode; +}; + +type UsageCardStateOptions = { + used: number; + limit: number | null | undefined; + footnote?: ReactNode; +}; + +const getUsageCardState = ({ used, limit, footnote }: UsageCardStateOptions): UsageCardState => { + const percent = getQuotaUsagePercent(used, limit ?? null); + const hasFiniteLimit = Boolean(limit && limit > 0); + + if (limit === null || limit === undefined) { + return { + status: { label: Unlimited, variant: 'neutral' }, + percent, + hasFiniteLimit, + progressClassName: '', + subtext: footnote ?? null, + }; + } + + if (limit === 0) { + return { + status: { label: Blocked, variant: 'destructive' }, + percent, + hasFiniteLimit, + progressClassName: '', + subtext: footnote ?? Resource blocked, + }; + } + + if (used > limit) { + return { + status: { label: Exceeded, variant: 'destructive' }, + percent, + hasFiniteLimit, + progressClassName: '[&>div]:bg-destructive', + subtext: footnote ?? null, + }; + } + + if (isQuotaExceeded(limit, used)) { + return { + status: { label: Limit reached, variant: 'orange' }, + percent, + hasFiniteLimit, + progressClassName: '[&>div]:bg-orange-500 dark:[&>div]:bg-orange-400', + subtext: footnote ?? null, + }; + } + + if (isQuotaNearing(limit, used)) { + return { + status: { label: Near limit, variant: 'warning' }, + percent, + hasFiniteLimit, + progressClassName: '[&>div]:bg-yellow-500 dark:[&>div]:bg-yellow-400', + subtext: footnote ?? null, + }; + } + + return { + status: { label: Within limit, variant: 'default' }, + percent, + hasFiniteLimit, + progressClassName: '', + subtext: footnote ?? null, + }; +}; + +type UsageStatCardProps = { + label: ReactNode; + icon: LucideIcon; + used: number; + limit: number | null | undefined; + /** When true the card is a plain counter with no limit, status or progress. */ + countOnly?: boolean; + footnote?: ReactNode; + action?: ReactNode; +}; + +const UsageStatCard = ({ label, icon: Icon, used, limit, countOnly = false, footnote, action }: UsageStatCardProps) => { + const { status, percent, hasFiniteLimit, progressClassName, subtext } = getUsageCardState({ used, limit, footnote }); + + return ( +
+
+
+ + {label} +
+ + {!countOnly && ( + + {status.label} + + )} +
+ +
+
+
+ + {used.toLocaleString()} + + {hasFiniteLimit ? ( + / {limit?.toLocaleString()} + ) : null} +
+ + {hasFiniteLimit ? ( + {percent}% + ) : null} +
+ + {hasFiniteLimit ? : null} + + {subtext ?

{subtext}

: null} +
+ + {action ?
{action}
: null} +
+ ); }; export const OrganisationUsagePanel = ({ organisationId, monthlyStats, organisationClaim, + capacityUsage, }: OrganisationUsagePanelProps) => { + const monthlyUsagePeriodId = useId(); const [selectedPeriod, setSelectedPeriod] = useState(() => monthlyStats[0]?.period); const selectedStat = monthlyStats.find((stat) => stat.period === selectedPeriod) ?? monthlyStats[0]; @@ -30,86 +193,105 @@ export const OrganisationUsagePanel = ({ // current period), so only offer the reset action when viewing the current month. const isCurrentPeriod = selectedStat?.period === currentMonthlyPeriod(); - const rows = [ + const capacityRows = capacityUsage + ? [ + { + key: 'members', + label: Members, + icon: UsersIcon, + used: capacityUsage.members, + limit: normalizeCapacityLimit(organisationClaim.memberCount), + }, + { + key: 'teams', + label: Teams, + icon: UsersRoundIcon, + used: capacityUsage.teams, + limit: normalizeCapacityLimit(organisationClaim.teamCount), + }, + ] + : []; + + const monthlyRows: UsageRow[] = [ { - counter: 'document' as const, + counter: 'document', label: Documents, + icon: FileIcon, used: selectedStat?.documentCount ?? 0, effectiveLimit: organisationClaim.documentQuota, }, { - counter: 'email' as const, + counter: 'email', label: Emails, + icon: MailIcon, used: selectedStat?.emailCount ?? 0, effectiveLimit: organisationClaim.emailQuota, }, { - counter: 'api' as const, + counter: 'api', label: API requests, + icon: PlugIcon, used: selectedStat?.apiCount ?? 0, effectiveLimit: organisationClaim.apiQuota, }, ]; return ( -
-
-

- Usage for period: {selectedStat?.period || 'N/A'} -

+
+ {capacityRows.length > 0 ? ( +
+ {capacityRows.map((row) => ( + + ))} +
+ ) : null} - {monthlyStats.length > 0 && ( - - )} -
+
+
+

+ Monthly usage +

- {rows.map((row) => { - const percent = - row.effectiveLimit && row.effectiveLimit > 0 - ? Math.min(100, Math.round((row.used / row.effectiveLimit) * 100)) - : 0; + {monthlyStats.length > 0 ? ( + + ) : null} +
- return ( -
-
- {row.label} - - {row.used} /{' '} - {match(row.effectiveLimit) - .with(null, () => Unlimited) - .with(0, () => Blocked) - .otherwise(String)} - -
+
+ {monthlyRows.map((row) => ( + + ) : undefined + } + /> + ))} - {row.effectiveLimit && row.effectiveLimit > 0 ? : null} - - {selectedStat && isCurrentPeriod && ( -
- -
- )} -
- ); - })} - -
-
- - Reports - - {selectedStat?.emailReports ?? 0} + Reports} + icon={MailOpenIcon} + used={selectedStat?.emailReports ?? 0} + limit={null} + countOnly + footnote={Sent this period} + />
diff --git a/apps/remix/app/components/general/organisation-usage-reset-button.tsx b/apps/remix/app/components/general/organisation-usage-reset-button.tsx index 7451f6e1c..bec8b8dd6 100644 --- a/apps/remix/app/components/general/organisation-usage-reset-button.tsx +++ b/apps/remix/app/components/general/organisation-usage-reset-button.tsx @@ -2,6 +2,7 @@ import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { Trans, useLingui } from '@lingui/react/macro'; +import { RotateCcwIcon } from 'lucide-react'; import { useRevalidator } from 'react-router'; type OrganisationUsageResetButtonProps = { @@ -32,6 +33,7 @@ export const OrganisationUsageResetButton = ({ organisationId, counter }: Organi loading={isPending} onClick={() => reset({ organisationId, counter })} > + Reset ); diff --git a/apps/remix/app/components/general/rate-limit-array-input.tsx b/apps/remix/app/components/general/rate-limit-array-input.tsx index a2adaad38..a0197764f 100644 --- a/apps/remix/app/components/general/rate-limit-array-input.tsx +++ b/apps/remix/app/components/general/rate-limit-array-input.tsx @@ -1,7 +1,9 @@ +import { RATE_LIMIT_WINDOW_REGEX } from '@documenso/lib/types/subscription'; import { Button } from '@documenso/ui/primitives/button'; import { Input } from '@documenso/ui/primitives/input'; -import { Trans } from '@lingui/react/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; import { PlusIcon, Trash2Icon } from 'lucide-react'; +import { useState } from 'react'; type RateLimitEntryValue = { window: string; max: number }; @@ -11,50 +13,153 @@ type RateLimitArrayInputProps = { disabled?: boolean; }; +const EMPTY_ENTRY: RateLimitEntryValue = { window: '', max: 0 }; + +/** A row counts as "started" once either field has input; fully-empty rows are dropped on commit. */ +const hasEntryInput = (entry: RateLimitEntryValue) => entry.window.trim() !== '' || entry.max > 0; + +/** Keep in-progress rows; drop rows that are completely empty. */ +const persistEntries = (entries: RateLimitEntryValue[]) => { + return entries.map((entry) => ({ ...entry, window: entry.window.trim() })).filter(hasEntryInput); +}; + export const RateLimitArrayInput = ({ value, onChange, disabled }: RateLimitArrayInputProps) => { - const entries = value ?? []; + const { t } = useLingui(); + const [draftEntry, setDraftEntry] = useState(null); + + const entries = draftEntry ? [...value, draftEntry] : value.length ? value : [EMPTY_ENTRY]; + + const getWindowError = (entry: RateLimitEntryValue, index: number) => { + const window = entry.window.trim(); + + if (!hasEntryInput(entry)) { + return null; + } + + if (window === '') { + return t`Enter a window, e.g. 5m`; + } + + if (!RATE_LIMIT_WINDOW_REGEX.test(window)) { + return t`Use a duration with a unit, e.g. 5m, 1h, or 24h`; + } + + const isDuplicateWindow = entries.some((otherEntry, otherIndex) => { + return otherIndex !== index && otherEntry.window.trim() === window; + }); + + return isDuplicateWindow ? t`Use a unique window for each rate limit` : null; + }; + + const getMaxError = (entry: RateLimitEntryValue) => { + if (!hasEntryInput(entry)) { + return null; + } + + return entry.max > 0 ? null : t`Enter a max request count greater than 0`; + }; const updateEntry = (index: number, patch: Partial) => { - const next = entries.map((entry, i) => (i === index ? { ...entry, ...patch } : entry)); - onChange(next); + if (index >= value.length) { + const nextDraftEntry = { ...(draftEntry ?? EMPTY_ENTRY), ...patch }; + + if (hasEntryInput(nextDraftEntry)) { + onChange(persistEntries([...value, nextDraftEntry])); + setDraftEntry(null); + return; + } + + setDraftEntry(nextDraftEntry); + return; + } + + const next = value.map((entry, i) => (i === index ? { ...entry, ...patch } : entry)); + onChange(persistEntries(next)); }; const removeEntry = (index: number) => { - onChange(entries.filter((_, i) => i !== index)); + if (index >= value.length) { + setDraftEntry(null); + return; + } + + const next = value.filter((_, i) => i !== index); + onChange(persistEntries(next)); }; const addEntry = () => { - onChange([...entries, { window: '5m', max: 100 }]); + setDraftEntry(EMPTY_ENTRY); }; + const hasErrors = entries.some((entry, index) => getWindowError(entry, index) || getMaxError(entry)); + const isAddDisabled = disabled || value.length === 0 || Boolean(draftEntry) || hasErrors; + return (
- {entries.map((entry, index) => ( -
- updateEntry(index, { window: e.target.value })} - /> - updateEntry(index, { max: parseInt(e.target.value, 10) || 0 })} - /> - -
- ))} +
+ + Window + + + Max requests + +
- +
+ + {windowError ?

{windowError}

: null} + {maxError ?

{maxError}

: null} +
+ ); + })} + +
); diff --git a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx index ac668ee3f..d895b338d 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx @@ -8,6 +8,7 @@ import { getHighestOrganisationRoleInGroup } from '@documenso/lib/utils/organisa import { trpc } from '@documenso/trpc/react'; import type { TGetAdminOrganisationResponse } from '@documenso/trpc/server/admin-router/get-admin-organisation.types'; import { ZUpdateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/update-admin-organisation.types'; +import { cn } from '@documenso/ui/lib/utils'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@documenso/ui/primitives/accordion'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Badge } from '@documenso/ui/primitives/badge'; @@ -30,7 +31,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans, useLingui } from '@lingui/react/macro'; -import { OrganisationMemberRole } from '@prisma/client'; +import { OrganisationMemberRole, SubscriptionStatus } from '@prisma/client'; import { ExternalLinkIcon, InfoIcon, Loader } from 'lucide-react'; import { useMemo } from 'react'; import { useForm } from 'react-hook-form'; @@ -42,7 +43,6 @@ import { AdminOrganisationDeleteDialog } from '~/components/dialogs/admin-organi import { AdminOrganisationMemberDeleteDialog } from '~/components/dialogs/admin-organisation-member-delete-dialog'; import { AdminOrganisationMemberUpdateDialog } from '~/components/dialogs/admin-organisation-member-update-dialog'; import { AdminOrganisationSyncSubscriptionDialog } from '~/components/dialogs/admin-organisation-sync-subscription-dialog'; -import { DetailsCard, DetailsValue } from '~/components/general/admin-details'; import { AdminGlobalSettingsSection } from '~/components/general/admin-global-settings-section'; import { ClaimLimitFields } from '~/components/general/claim-limit-fields'; import { GenericErrorLayout } from '~/components/general/generic-error-layout'; @@ -268,54 +268,32 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro -
-
-
-

- Organisation usage -

-

- Current usage against organisation limits. -

-
-
+ -
- Members}> - - {organisation.members.length} /{' '} - {organisation.organisationClaim.memberCount === 0 - ? t`Unlimited` - : organisation.organisationClaim.memberCount} - - - - Teams}> - - {organisation.teams.length} /{' '} - {organisation.organisationClaim.teamCount === 0 ? t`Unlimited` : organisation.organisationClaim.teamCount} - - -
- -
- -
-
+
-

+

Global Settings

-

+

Default settings applied to this organisation.

@@ -335,7 +313,15 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro className="mt-16" /> - +
Subscription @@ -343,7 +329,12 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro {organisation.subscription ? ( - {i18n._(SUBSCRIPTION_STATUS_MAP[organisation.subscription.status])} subscription found + + {organisation.subscription.status === SubscriptionStatus.ACTIVE && ( + ) : ( No subscription found @@ -356,6 +347,7 @@ export default function OrganisationGroupSettingsPage({ params, loaderData }: Ro
} /> - From b16f979eb33622aee5a85252be501bbeae0eb9f9 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 14 Jul 2026 16:44:22 +0800 Subject: [PATCH 32/41] fix: improve editor autosave (#3057) --- .../envelope-recipient-autosave-race.spec.ts | 199 ++++++++++++++++++ .../hooks/use-envelope-autosave.ts | 116 +++++----- 2 files changed, 265 insertions(+), 50 deletions(-) create mode 100644 packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-autosave-race.spec.ts diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-autosave-race.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-autosave-race.spec.ts new file mode 100644 index 000000000..1d1035527 --- /dev/null +++ b/packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-autosave-race.spec.ts @@ -0,0 +1,199 @@ +import { prisma } from '@documenso/prisma'; +import { expect, type Page, test } from '@playwright/test'; + +import { + clickAddSignerButton, + clickEnvelopeEditorStep, + getRecipientEmailInputs, + openDocumentEnvelopeEditor, + setRecipientEmail, + setRecipientName, + type TEnvelopeEditorSurface, +} from '../fixtures/envelope-editor'; + +/** + * Reproduction for the recipient autosave race condition. + * + * Symptom (production only, where there is real network lag): + * 1. The author adds a recipient and types its name/email. + * 2. They navigate to the "Add Fields" step. + * 3. The recipient selector shows the default "Recipient 1" placeholder + * instead of the recipient they just typed, and the typed name/email is + * silently lost. + * + * Theory (see packages/lib/client-only/hooks/use-envelope-autosave.ts): + * When the author navigates, `flushAutosave()` is awaited before the Add + * Fields page renders. If an *earlier* (empty) recipient save is still + * in-flight at that moment, `flush()` awaits that in-flight save and returns + * WITHOUT committing the newer typed data sitting in `lastArgsRef` (whose + * debounce timer it just cleared). The typed data is dropped, the empty + * recipient persists, and the selector renders "Recipient 1". + * + * This only happens when a save is still in-flight at navigation time, which is + * why it never reproduces locally (fast saves) but does on a laggy network. + * + * The test below simulates that lag by holding the first `envelope.recipient.set` + * request open. It asserts the CORRECT behaviour (typed recipient survives), so + * it is RED while the bug exists and GREEN once the autosave hook is fixed. + */ + +const RECIPIENT_SET_PROCEDURE = 'envelope.recipient.set'; + +// How long to hold the first recipient autosave "in-flight" to emulate prod lag. +const SIMULATED_NETWORK_LAG_MS = 5000; + +const FIRST_RECIPIENT = { + name: 'Alice Author', + email: 'alice-autosave-race@example.com', +}; + +const SECOND_RECIPIENT = { + name: 'Bob Builder', + email: 'bob-autosave-race@example.com', +}; + +type RecipientSetLagHandle = { + /** Resolves the instant the first recipient.set request is in-flight on the client. */ + firstRecipientSetInFlight: Promise; + /** Raw request bodies of every recipient.set call we intercepted. */ + recipientSetRequestBodies: string[]; +}; + +/** + * Installs a fake "production network lag" on the recipient autosave mutation. + * + * Only the FIRST recipient.set request is held open for `lagMs` (this is the save + * that must still be in-flight at navigation time for the race to occur). It + * resolves `firstRecipientSetInFlight` the instant it is intercepted so the test + * can keep typing while that save is pending. Subsequent recipient.set requests + * (e.g. the follow-up save the fixed hook issues) are forwarded immediately so the + * test does not pay the lag twice. + */ +const installRecipientSetLag = async (page: Page, lagMs: number): Promise => { + let markFirstInFlight: () => void = () => {}; + + const firstRecipientSetInFlight = new Promise((resolve) => { + markFirstInFlight = resolve; + }); + + const recipientSetRequestBodies: string[] = []; + + await page.route('**/api/trpc/**', async (route) => { + const request = route.request(); + + if (request.method() !== 'POST' || !request.url().includes(RECIPIENT_SET_PROCEDURE)) { + await route.continue(); + return; + } + + const callIndex = recipientSetRequestBodies.length + 1; + recipientSetRequestBodies.push(request.postData() ?? ''); + + if (callIndex === 1) { + // eslint-disable-next-line no-console + console.log(`[test] holding first ${RECIPIENT_SET_PROCEDURE} for ${lagMs}ms (simulated network lag)`); + + // The empty save is now in-flight from the client's perspective. + markFirstInFlight(); + + await new Promise((resolve) => setTimeout(resolve, lagMs)); + } else { + // eslint-disable-next-line no-console + console.log(`[test] forwarding ${RECIPIENT_SET_PROCEDURE} #${callIndex} (no lag)`); + } + + await route.continue(); + }); + + return { firstRecipientSetInFlight, recipientSetRequestBodies }; +}; + +const assertEnvelopeRecipientsPersisted = async (surface: TEnvelopeEditorSurface) => { + if (!surface.envelopeId) { + throw new Error('Expected the document editor surface to have an envelopeId'); + } + + const envelope = await prisma.envelope.findFirstOrThrow({ + where: { id: surface.envelopeId }, + include: { + recipients: { + orderBy: { signingOrder: 'asc' }, + }, + }, + }); + + const persistedEmails = envelope.recipients.map((recipient) => recipient.email).filter(Boolean); + + // eslint-disable-next-line no-console + console.log( + '[test] persisted recipients:', + JSON.stringify( + envelope.recipients.map((recipient) => ({ name: recipient.name, email: recipient.email })), + null, + 2, + ), + ); + + expect(persistedEmails).toContain(FIRST_RECIPIENT.email); + expect(persistedEmails).toContain(SECOND_RECIPIENT.email); +}; + +test.describe('envelope editor recipient autosave race (network lag)', () => { + test('document editor: typed recipient survives navigation to Add Fields', async ({ page }) => { + const surface = await openDocumentEnvelopeEditor(page); + + const { firstRecipientSetInFlight, recipientSetRequestBodies } = await installRecipientSetLag( + page, + SIMULATED_NETWORK_LAG_MS, + ); + + // 1. Add a second signer row. A blank document already has one empty default + // signer, so this schedules an autosave of TWO empty recipients + // (name='' / email='') - this is the save that will be in-flight. + await clickAddSignerButton(surface.root); + await expect(getRecipientEmailInputs(surface.root)).toHaveCount(2); + + // 2. Wait until that empty autosave is actually in-flight on the client. This + // is the precondition the bug needs: a slow save holding the autosave lock. + await firstRecipientSetInFlight; + + // 3. The author now fills in the recipients they are adding. + await setRecipientName(surface.root, 0, FIRST_RECIPIENT.name); + await setRecipientEmail(surface.root, 0, FIRST_RECIPIENT.email); + await setRecipientName(surface.root, 1, SECOND_RECIPIENT.name); + await setRecipientEmail(surface.root, 1, SECOND_RECIPIENT.email); + + // 4. Immediately navigate to Add Fields (before the typed data's debounce + // fires). flushAutosave() awaits the in-flight EMPTY save; with the bug + // present it returns without ever committing the typed data. + await clickEnvelopeEditorStep(surface.root, 'addFields'); + + // 5. Wait for the Add Fields page to render (after the lagged flush resolves). + await expect(surface.root.getByText('Selected Recipient')).toBeVisible({ + timeout: SIMULATED_NETWORK_LAG_MS + 15000, + }); + + // Diagnostics - the request bodies show what actually reached the server. + // Buggy: only the first (empty) save is ever sent. Fixed: a follow-up save + // carrying the typed recipients is sent too. + // eslint-disable-next-line no-console + console.log('\n===== AUTOSAVE RACE DIAGNOSTICS ====='); + // eslint-disable-next-line no-console + console.log(`recipient.set requests sent to server: ${recipientSetRequestBodies.length}`); + // eslint-disable-next-line no-console + console.log( + `server ever received "${FIRST_RECIPIENT.email}": ${recipientSetRequestBodies.some((body) => body.includes(FIRST_RECIPIENT.email))}`, + ); + // eslint-disable-next-line no-console + console.log('=====================================\n'); + + // 6. THE USER-VISIBLE BUG: the selected recipient must be the one we typed + // (Alice), not the default "Recipient 1" placeholder. + const selectedRecipientSection = surface.root.locator('section').filter({ hasText: 'Selected Recipient' }); + + await expect(selectedRecipientSection.getByRole('combobox')).toContainText(FIRST_RECIPIENT.name); + + // 7. THE DATA LOSS: the typed recipients must actually be persisted. + await assertEnvelopeRecipientsPersisted(surface); + }); +}); diff --git a/packages/lib/client-only/hooks/use-envelope-autosave.ts b/packages/lib/client-only/hooks/use-envelope-autosave.ts index 1ca70c28c..93cc8a162 100644 --- a/packages/lib/client-only/hooks/use-envelope-autosave.ts +++ b/packages/lib/client-only/hooks/use-envelope-autosave.ts @@ -1,84 +1,100 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +/** + * Debounced autosave for the envelope editor (recipients, fields, settings). + * + * Only one save runs at a time and the latest edit always wins. If the user + * keeps editing while a save is on the wire, their newest changes get saved + * right after, never dropped. + */ export function useEnvelopeAutosave(saveFn: (data: T) => Promise, delay = 1000) { const timeoutRef = useRef | null>(null); - const lastArgsRef = useRef(null); - const pendingPromiseRef = useRef | null>(null); + + // The edit waiting to be saved. Wrapped in an object so null always means "nothing queued". + const pendingRef = useRef<{ value: T } | null>(null); + + // The save currently running, if any. Shared so we never kick off two at once. + const commitPromiseRef = useRef | null>(null); + + // saveFn closes over editor state, so keep the latest one around without + // making triggerSave/flush depend on it. + const saveFnRef = useRef(saveFn); + saveFnRef.current = saveFn; const [isPending, setIsPending] = useState(false); const [isCommiting, setIsCommiting] = useState(false); + /** + * Runs saves one at a time until the queue is empty. Anything queued + * mid-save gets picked up on the next loop. + */ + const commit = useCallback((): Promise => { + if (commitPromiseRef.current) { + return commitPromiseRef.current; + } + + if (!pendingRef.current) { + return Promise.resolve(); + } + + const pump = (async () => { + try { + setIsCommiting(true); + + while (pendingRef.current) { + const { value } = pendingRef.current; + pendingRef.current = null; + + await saveFnRef.current(value); + } + } finally { + // eslint-disable-next-line require-atomic-updates + commitPromiseRef.current = null; + setIsCommiting(false); + setIsPending(false); + } + })(); + + commitPromiseRef.current = pump; + + return pump; + }, []); + const triggerSave = useCallback( (data: T) => { - lastArgsRef.current = data; + pendingRef.current = { value: data }; - // A debounce or promise means something is pending setIsPending(true); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } - // eslint-disable-next-line @typescript-eslint/no-misused-promises - timeoutRef.current = setTimeout(async () => { - if (!lastArgsRef.current) { - return; - } - - const args = lastArgsRef.current; - lastArgsRef.current = null; + timeoutRef.current = setTimeout(() => { timeoutRef.current = null; - - setIsCommiting(true); - pendingPromiseRef.current = saveFn(args); - - try { - await pendingPromiseRef.current; - } finally { - // eslint-disable-next-line require-atomic-updates - pendingPromiseRef.current = null; - setIsCommiting(false); - setIsPending(false); - } + void commit(); }, delay); }, - [saveFn, delay], + [commit, delay], ); + /** + * Skip the debounce and save now. The editor calls this when it needs + * everything persisted, e.g. before sending or switching steps. + */ const flush = useCallback(async () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } - if (pendingPromiseRef.current) { - // Already running → wait for it - await pendingPromiseRef.current; - return; - } - - if (lastArgsRef.current) { - const args = lastArgsRef.current; - lastArgsRef.current = null; - - setIsCommiting(true); - setIsPending(true); - - pendingPromiseRef.current = saveFn(args); - try { - await pendingPromiseRef.current; - } finally { - // eslint-disable-next-line require-atomic-updates - pendingPromiseRef.current = null; - setIsCommiting(false); - setIsPending(false); - } - } - }, [saveFn]); + await commit(); + }, [commit]); + // Last-ditch attempt to save if the tab closes with unsaved edits. useEffect(() => { const handleBeforeUnload = () => { - if (timeoutRef.current || pendingPromiseRef.current) { + if (timeoutRef.current || pendingRef.current || commitPromiseRef.current) { void flush(); } }; From 12223c79cb6469108480bbd5c42c3aa39708a3de Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:40:40 +0000 Subject: [PATCH 33/41] fix(ui): improve destructive button contrast in dark mode (#3071) --- apps/docs/src/app/global.css | 2 +- packages/ui/styles/theme.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css index 7d9c89316..ce71134c8 100644 --- a/apps/docs/src/app/global.css +++ b/apps/docs/src/app/global.css @@ -83,7 +83,7 @@ --accent: hsl(0 0% 27.8431%); --accent-foreground: hsl(95.0847 71.0843% 67.451%); --destructive: hsl(0 86.5979% 61.9608%); - --destructive-foreground: hsl(0 87.6289% 19.0196%); + --destructive-foreground: hsl(0 0% 98.0392%); --border: hsl(0 0% 27.8431%); --input: hsl(0 0% 27.8431%); --ring: hsl(95.0847 71.0843% 67.451%); diff --git a/packages/ui/styles/theme.css b/packages/ui/styles/theme.css index caa767433..fb125c026 100644 --- a/packages/ui/styles/theme.css +++ b/packages/ui/styles/theme.css @@ -174,7 +174,7 @@ --accent-foreground: 95.08 71.08% 67.45%; --destructive: 0 87% 62%; - --destructive-foreground: 0 87% 19%; + --destructive-foreground: 0 0% 98%; --ring: 95.08 71.08% 67.45%; From d6268b1d7df560d984cb019ea719c972a68b6ee7 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 14 Jul 2026 19:13:40 +0800 Subject: [PATCH 34/41] fix: missing noreply email for resend (#3094) --- package-lock.json | 22 ++++---- package.json | 2 +- packages/email/package.json | 4 +- packages/email/transports/mailchannels.ts | 5 ++ .../email/transports/normalize-headers.ts | 55 +++++++++++++++++++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 packages/email/transports/normalize-headers.ts diff --git a/package-lock.json b/package-lock.json index ac1c7d2f1..4f4159749 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "inngest-cli": "^1.17.9", "lint-staged": "^16.2.7", "nanoid": "^5.1.6", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "pdfjs-dist": "5.4.296", "pino": "^9.14.0", "pino-pretty": "^13.1.2", @@ -3048,15 +3048,15 @@ "link": true }, "node_modules/@documenso/nodemailer-resend": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-4.0.0.tgz", - "integrity": "sha512-o2Wpz8jJXOov+CbEzWUhqXlBsdx/l/W0au054j5HUExaLpS+sCoJfdOM0A5uHGTInWOxqbNB0WSvxf9dWAzVSg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@documenso/nodemailer-resend/-/nodemailer-resend-5.0.0.tgz", + "integrity": "sha512-PDC7Yjzg35cS5i2TSJEZSP9j86BsYv4pEZOODurkK8+KFd3ymtWUqJrM6PrSNQ4MQpFp7chHlbBFP0ALYl8n0A==", "license": "MIT", "dependencies": { - "resend": "^4.0.0" + "resend": "^4.8.0" }, "peerDependencies": { - "nodemailer": "^6.9.3" + "nodemailer": "^9.0.0" } }, "node_modules/@documenso/nodemailer-resend/node_modules/@react-email/render": { @@ -24494,9 +24494,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz", - "integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.3.tgz", + "integrity": "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -31270,7 +31270,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@documenso/nodemailer-resend": "4.0.0", + "@documenso/nodemailer-resend": "5.0.0", "@documenso/tailwind-config": "*", "@react-email/body": "0.2.0", "@react-email/button": "0.2.0", @@ -31291,7 +31291,7 @@ "@react-email/section": "0.0.16", "@react-email/tailwind": "^2.0.1", "@react-email/text": "0.1.5", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "react-email": "^5.0.6", "resend": "^6.5.2" }, diff --git a/package.json b/package.json index 808577cbc..2800c7f84 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "inngest-cli": "^1.17.9", "lint-staged": "^16.2.7", "nanoid": "^5.1.6", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "pdfjs-dist": "5.4.296", "pino": "^9.14.0", "pino-pretty": "^13.1.2", diff --git a/packages/email/package.json b/packages/email/package.json index bb549f70b..5fbc4a88c 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -17,7 +17,7 @@ "clean": "rimraf node_modules" }, "dependencies": { - "@documenso/nodemailer-resend": "4.0.0", + "@documenso/nodemailer-resend": "5.0.0", "@documenso/tailwind-config": "*", "@react-email/body": "0.2.0", "@react-email/button": "0.2.0", @@ -38,7 +38,7 @@ "@react-email/section": "0.0.16", "@react-email/tailwind": "^2.0.1", "@react-email/text": "0.1.5", - "nodemailer": "^8.0.5", + "nodemailer": "^9.0.0", "react-email": "^5.0.6", "resend": "^6.5.2" }, diff --git a/packages/email/transports/mailchannels.ts b/packages/email/transports/mailchannels.ts index 93e82804d..923346f79 100644 --- a/packages/email/transports/mailchannels.ts +++ b/packages/email/transports/mailchannels.ts @@ -3,6 +3,8 @@ import type { SentMessageInfo, Transport } from 'nodemailer'; import type { Address } from 'nodemailer/lib/mailer'; import type MailMessage from 'nodemailer/lib/mailer/mail-message'; +import { normalizeMailHeaders } from './normalize-headers'; + const VERSION = '1.0.0'; type NodeMailerAddress = string | Address | Array | undefined; @@ -54,6 +56,7 @@ export class MailChannelsTransport implements Transport { const mailBcc = this.toMailChannelsAddresses(mail.data.bcc); const [from] = this.toMailChannelsAddresses(mail.data.from); + const [replyTo] = this.toMailChannelsAddresses(mail.data.replyTo); if (!from) { return callback(new Error('Missing required field "from"'), null); @@ -72,6 +75,8 @@ export class MailChannelsTransport implements Transport { headers: requestHeaders, body: JSON.stringify({ from: from, + reply_to: replyTo, + headers: normalizeMailHeaders(mail.data.headers), subject: mail.data.subject, personalizations: [ { diff --git a/packages/email/transports/normalize-headers.ts b/packages/email/transports/normalize-headers.ts new file mode 100644 index 000000000..4dca6c8f1 --- /dev/null +++ b/packages/email/transports/normalize-headers.ts @@ -0,0 +1,55 @@ +import type Mail from 'nodemailer/lib/mailer'; + +/** + * Normalizes nodemailer mail headers into the flat `Record` + * shape accepted by HTTP email APIs such as Resend and MailChannels. + * + * Kept in sync with `toResendHeaders` in the `@documenso/nodemailer-resend` + * package, which applies the same normalization for the Resend transport. + */ +export const normalizeMailHeaders = (headers: Mail.Options['headers']): Record | undefined => { + if (!headers) { + return undefined; + } + + const normalized: Record = {}; + + const appendHeader = (key: string, value: unknown) => { + if (value === null || value === undefined) { + return; + } + + const stringValue = String(value); + + normalized[key] = normalized[key] ? `${normalized[key]}, ${stringValue}` : stringValue; + }; + + if (Array.isArray(headers)) { + for (const { key, value } of headers) { + appendHeader(key, value); + } + } else { + for (const [key, value] of Object.entries(headers)) { + if (Array.isArray(value)) { + for (const item of value) { + appendHeader(key, item); + } + + continue; + } + + if (typeof value === 'object' && value !== null) { + appendHeader(key, value.value); + continue; + } + + appendHeader(key, value); + } + } + + if (Object.keys(normalized).length === 0) { + return undefined; + } + + return normalized; +}; From 5c41740859104380f0c1561a2dc40cb11b63e2da Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Tue, 14 Jul 2026 23:15:45 +1000 Subject: [PATCH 35/41] chore: deps upgrade 2026-07-14 (#3095) --- .npmrc | 2 +- apps/docs/package.json | 4 +- apps/remix/package.json | 2 +- docker/Dockerfile | 4 +- package-lock.json | 4993 ++++++++++++++++++------- package.json | 4 +- packages/app-tests/package.json | 2 +- packages/email/package.json | 2 +- packages/lib/package.json | 4 +- packages/prisma/package.json | 2 +- packages/tailwind-config/package.json | 2 +- turbo.json | 3 +- 12 files changed, 3593 insertions(+), 1431 deletions(-) diff --git a/.npmrc b/.npmrc index cbc6b6537..75baad7f0 100644 --- a/.npmrc +++ b/.npmrc @@ -1,3 +1,3 @@ legacy-peer-deps = true prefer-dedupe = true -min-release-age = 7 +# min-release-age = 7 diff --git a/apps/docs/package.json b/apps/docs/package.json index 76aecb716..da9966679 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "build": "next build", + "build": "NEXT_IGNORE_INCORRECT_LOCKFILE=true next build", "dev": "next dev", "start": "next start", "types:check": "fumadocs-mdx && next typegen && tsc --noEmit", @@ -29,7 +29,7 @@ "@types/node": "^25.1.0", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "tailwindcss": "^4.1.18", "typescript": "^5.9.3" } diff --git a/apps/remix/package.json b/apps/remix/package.json index b44c2e273..1ea895f2c 100644 --- a/apps/remix/package.json +++ b/apps/remix/package.json @@ -100,7 +100,7 @@ "esbuild": "^0.27.0", "remix-flat-routes": "^0.8.5", "rollup": "^4.53.3", - "tsx": "^4.20.6", + "tsx": "^4.23.1", "typescript": "5.6.2", "vite": "^7.2.4", "vite-plugin-babel-macros": "^1.0.6", diff --git a/docker/Dockerfile b/docker/Dockerfile index 47304ba8d..59818689f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,7 +19,7 @@ WORKDIR /app COPY . . -RUN npm install -g "turbo@^1.9.3" +RUN npm install -g "turbo@^2.10.0" # Outputs to the /out folder # source: https://turbo.build/repo/docs/reference/command-line-reference/prune#--docker @@ -79,7 +79,7 @@ COPY --from=builder /app/out/full/ . # Finally copy the turbo.json file so that we can run turbo commands COPY turbo.json turbo.json -RUN npm install -g "turbo@^1.9.3" +RUN npm install -g "turbo@^2.10.0" RUN turbo run build --filter=@documenso/remix... diff --git a/package-lock.json b/package-lock.json index 4f4159749..3e984b050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@ts-rest/serverless": "^3.52.1", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", + "esbuild": "^0.27.0", "husky": "^9.1.7", "inngest": "^3.54.0", "inngest-cli": "^1.17.9", @@ -61,7 +62,7 @@ "rimraf": "^6.1.2", "superjson": "^2.2.5", "syncpack": "^14.0.0-alpha.27", - "turbo": "^1.13.4", + "turbo": "^2.10.0", "vite": "^7.2.4", "vite-plugin-static-copy": "^3.1.4", "zod-openapi": "^4.2.4", @@ -96,15 +97,15 @@ "@types/node": "^25.1.0", "@types/react": "^19.2.10", "@types/react-dom": "^19.2.3", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "tailwindcss": "^4.1.18", "typescript": "^5.9.3" } }, "apps/docs/node_modules/@types/node": { - "version": "25.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", - "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -146,47 +147,6 @@ "url": "https://paulmillr.com/funding/" } }, - "apps/docs/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, "apps/docs/node_modules/fumadocs-mdx": { "version": "14.2.6", "resolved": "https://registry.npmjs.org/fumadocs-mdx/-/fumadocs-mdx-14.2.6.tgz", @@ -494,17 +454,478 @@ "esbuild": "^0.27.0", "remix-flat-routes": "^0.8.5", "rollup": "^4.53.3", - "tsx": "^4.20.6", + "tsx": "^4.23.1", "typescript": "5.6.2", "vite": "^7.2.4", "vite-plugin-babel-macros": "^1.0.6", "vite-tsconfig-paths": "^5.1.4" } }, - "apps/remix/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "apps/remix/node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "apps/remix/node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -515,32 +936,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/@ai-sdk/anthropic": { @@ -560,9 +981,9 @@ } }, "node_modules/@ai-sdk/gateway": { - "version": "2.0.107", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.107.tgz", - "integrity": "sha512-4Yjo7U6KqcePqsMHL7g0NcwwQQh1IQ3gyW16LEXKlBX5C4pWxXM0qUyxRnNohyVAMKDW2VSWL4BViNNVqQoIDw==", + "version": "2.0.111", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.111.tgz", + "integrity": "sha512-pZR4Is/0IGH1tDY6XgJltoH3hxVqZ+2StQMhWkBtCWWaa4JYKP8gAiNPDxy585HenQfcAqr1qsO9FgSYkHxo5w==", "license": "Apache-2.0", "dependencies": { "@ai-sdk/provider": "2.0.3", @@ -3905,9 +4326,9 @@ } }, "node_modules/@hono/vite-dev-server/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -4490,9 +4911,9 @@ } }, "node_modules/@inngest/ai/node_modules/@types/node": { - "version": "22.20.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", - "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4753,9 +5174,9 @@ } }, "node_modules/@libpdf/core/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -4855,6 +5276,490 @@ "node": ">=20.0.0" } }, + "node_modules/@lingui/cli/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@lingui/cli/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/@lingui/conf": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@lingui/conf/-/conf-5.6.0.tgz", @@ -6088,15 +6993,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6419,15 +7329,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-amqplib/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6475,15 +7390,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-aws-lambda/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6530,15 +7450,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-aws-sdk/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6585,15 +7510,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-bunyan/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6639,15 +7569,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-cassandra-driver/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6695,15 +7630,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-connect/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-connect/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6749,15 +7689,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-cucumber/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6802,15 +7747,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-dataloader/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6855,15 +7805,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-dns/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-dns/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6910,15 +7865,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-express/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-express/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -6964,15 +7924,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-fs/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-fs/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7017,15 +7982,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-generic-pool/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7070,15 +8040,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-graphql/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-graphql/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7124,15 +8099,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-grpc/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-grpc/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7179,15 +8159,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-hapi/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-hapi/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7233,15 +8218,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7289,15 +8279,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-http/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7344,15 +8339,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7398,15 +8398,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-kafkajs/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7452,15 +8457,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-knex/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-knex/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7507,15 +8517,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-koa/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-koa/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7560,15 +8575,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-lru-memoizer/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7615,15 +8635,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-memcached/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-memcached/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7669,15 +8694,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-mongodb/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7724,15 +8754,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-mongoose/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7779,15 +8814,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-mysql/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-mysql/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7834,15 +8874,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-mysql2/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7888,15 +8933,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-nestjs-core/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7942,15 +8992,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-net/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-net/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -7997,15 +9052,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-openai/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-openai/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8052,15 +9112,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-oracledb/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8110,15 +9175,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-pg/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-pg/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8165,15 +9235,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-pino/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-pino/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8220,15 +9295,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-redis/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-redis/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8275,15 +9355,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-restify/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-restify/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8329,15 +9414,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-router/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-router/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8384,15 +9474,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8437,15 +9532,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-socket.io/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8492,15 +9592,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-tedious/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-tedious/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8547,15 +9652,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-undici/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-undici/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8601,15 +9711,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/instrumentation-winston/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/instrumentation-winston/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -8831,9 +9946,9 @@ } }, "node_modules/@opentelemetry/resource-detector-gcp/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -9096,15 +10211,20 @@ "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, + "node_modules/@opentelemetry/sdk-node/node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/@opentelemetry/sdk-node/node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -12561,12 +13681,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.28.0.tgz", - "integrity": "sha512-N/LoLG8pZ1zv5cIWpdF6vmSjtZtXKK9G0OqT5yYCOZU+CzPq1+nYA95VoKJBGWRScs7YbMugZ7lZx8Fj1vdHoA==", + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.3.tgz", + "integrity": "sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.15.0", + "@smithy/types": "^4.16.1", "tslib": "^2.6.2" }, "engines": { @@ -12733,12 +13853,12 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.4.4.tgz", - "integrity": "sha512-Um/zfIqXidHeJzDbmQKTkm8vs8qSh+rrJsCCxVHcc6XKQpC7LanxQNVDsJ6NZ6l6Vmqp6S1+Ym/klQ+MPdvGew==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.4.8.tgz", + "integrity": "sha512-RoxyFKZVk+UVz51s1PIyjtVUhwTYlAaT78Xpzix7w5gflovJoJO7eqFxWK0Vg3BM3EbPh8NPLlwLPXsiYdBXoA==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.28.0", + "@smithy/core": "^3.29.3", "tslib": "^2.6.2" }, "engines": { @@ -12986,9 +14106,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", - "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -13050,12 +14170,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.4.4.tgz", - "integrity": "sha512-d9QySE58szTj0YlYQ5KRWfAtK2K7DECk1T/NKuWOIzg+II8agqv3sx41rvtfWbUTMsTMZMpURtS2Xgmnj7Xupg==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.4.8.tgz", + "integrity": "sha512-v89h2SviTW7rJ+KCyCDnXRSNbm/wbEdALdErm9UWpDsxHzq9F4mJn2uYiEUEWL81oudj0oua3xaGS68eHbCfkg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.28.0", + "@smithy/core": "^3.29.3", "tslib": "^2.6.2" }, "engines": { @@ -13192,12 +14312,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.4.4.tgz", - "integrity": "sha512-kvxCWygmILHgwuIQKxocTHblTsF1eWLF/rBN5qjY7QmHfIglcqJoe/p9mo7uOR/dA+h3eVZVLZcmscxp+WtDCA==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.4.8.tgz", + "integrity": "sha512-hwVELJTTRUqwrEvMI73PwsP27cO1HgkAKDoKelhMS3biTd8z5iXj76UF7oemuDba3X5eHZqjedeyBUhJLns+Kg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.28.0", + "@smithy/core": "^3.29.3", "tslib": "^2.6.2" }, "engines": { @@ -13791,6 +14911,90 @@ } } }, + "node_modules/@turbo/darwin-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.10.5.tgz", + "integrity": "sha512-ENvPwy3x5yS7MwNYHeWjqOBXkwIMp39Pd+/zXC6PoiNzF8EIvvLZOZZ+ny6L9x4WgS5vxUii2LM5gM+zjPdnWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.10.5.tgz", + "integrity": "sha512-rqROo9zsF/P9RqsdtbLD1nFJicjSrYyvQ9kNJC38AbxA3pAs6VAlATvtvOFx7bqOv6vicf20SP9kF33avJjy2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.10.5.tgz", + "integrity": "sha512-RoSSiNFUxi27zLJuM9F6GyWWjHgLch9t6nwD6K0FkXRirZkTLlzIj6IhFnK8H9++nefLtdFqylE4vGjZAv6AAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.10.5.tgz", + "integrity": "sha512-4ZComcpzmHGmVynQqvvi+iZOSq/tBvY1SltXB8g4NZRsrA01W8E+yRL8RNM+PLoyWsrCnJa8xa+DkWkv+xg4iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.10.5.tgz", + "integrity": "sha512-eL2Iyj4DbMINq1Sr1w0iAi6nAiZOF16KSlRGwCJpVh+IWZeY33MAsLHVOBMj1xoFtncVJXclCVpTPL2nBoYkFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.10.5.tgz", + "integrity": "sha512-sog+wP+8YSJrdWZ/rUJg8xghVTrwoG+BrSlDQpnK5fzSgJHn1INRWXbVWRH0d3vX8dBI01E3yxXRre9Dn+OXQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -14308,9 +15512,9 @@ } }, "node_modules/@types/nodemailer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", - "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", "dev": true, "license": "MIT", "dependencies": { @@ -14734,12 +15938,12 @@ } }, "node_modules/ai": { - "version": "5.0.208", - "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.208.tgz", - "integrity": "sha512-IiN5PCuUr4AcWhfFMJzirU6WVyZl3vrWbTN0BRnLWhrklNQ1W2VIoJ+H7mBLHzhC21kgv6dJkxsD6yc0M2lXNQ==", + "version": "5.0.212", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.212.tgz", + "integrity": "sha512-NmSi5BHrQpWNzCeisFqXLWyxyDbERIdvlJ7rNeIk8CB/GUqkKAPoEDn75uTuL3h9SCfFGVW5HTrRE8nX+ydqAg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "2.0.107", + "@ai-sdk/gateway": "2.0.111", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.28", "@opentelemetry/api": "1.9.0" @@ -15921,9 +17125,9 @@ } }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", - "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "dev": true, "license": "MIT", "dependencies": { @@ -16629,9 +17833,9 @@ } }, "node_modules/conf/node_modules/type-fest": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", - "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -16853,9 +18057,9 @@ } }, "node_modules/cron-parser": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.6.1.tgz", - "integrity": "sha512-QBm4o1PwZiuY7KFbVvW7FLC8bozy7YWzv+Fz6KRS7sQghzcbDZCGxr/Bc5b6TQreAoSwuWVP491dIcK0THCX6A==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-5.6.2.tgz", + "integrity": "sha512-yJ/G1LVir6CnlkLI40CsampPLKl1SprGGlUagWtGJxewytRVYezv5xVyzzbT+Pvzx+VIRvZVF7/a0eEMTxdnTA==", "license": "MIT", "dependencies": { "luxon": "^3.7.2" @@ -18316,9 +19520,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -18328,448 +19532,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -19331,9 +20119,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -20054,9 +20842,9 @@ } }, "node_modules/google-gax/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -20075,9 +20863,9 @@ } }, "node_modules/google-gax/node_modules/gaxios": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", - "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", + "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -23865,19 +24653,23 @@ "license": "BSD-3-Clause" }, "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -23895,18 +24687,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/motion-dom": { "version": "12.23.23", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", @@ -24348,9 +25128,9 @@ } }, "node_modules/next/node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -25263,9 +26043,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -25434,9 +26214,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -25692,9 +26472,9 @@ "license": "MIT-0" }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "funding": [ { "type": "opencollective", @@ -25860,9 +26640,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -27271,9 +28051,9 @@ } }, "node_modules/resend": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.16.0.tgz", - "integrity": "sha512-SaKISwHtxvAxneF84Njgnzg+zdngUu1vOT/paRU1De9QF+zXQR3GnwJHSh+mpJPjUhsGD4WxYi5CfKkXMkDqwg==", + "version": "6.17.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.17.2.tgz", + "integrity": "sha512-hbaXEORFIFfT2Bh03NsA/akTTTKkD1hiuJ88ke64c5dWVV6DyoLxzFve3OiZqVOQ+JKLJ5uVkPR6hlUslpJFoA==", "license": "MIT", "dependencies": { "postal-mime": "2.7.4", @@ -29350,6 +30130,490 @@ "fsevents": "~2.3.3" } }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/tsyringe": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", @@ -29369,107 +30633,23 @@ "license": "0BSD" }, "node_modules/turbo": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.13.4.tgz", - "integrity": "sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==", + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.10.5.tgz", + "integrity": "sha512-07Y/C7OUp23l4P92PJoYtFNbHjLhftrZH5Ce7dbczS4kX2Re+wtbXvZLoxn/pUtzgsQaRCBaRuZPJp4zmAn0WQ==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "1.13.4", - "turbo-darwin-arm64": "1.13.4", - "turbo-linux-64": "1.13.4", - "turbo-linux-arm64": "1.13.4", - "turbo-windows-64": "1.13.4", - "turbo-windows-arm64": "1.13.4" + "@turbo/darwin-64": "2.10.5", + "@turbo/darwin-arm64": "2.10.5", + "@turbo/linux-64": "2.10.5", + "@turbo/linux-arm64": "2.10.5", + "@turbo/windows-64": "2.10.5", + "@turbo/windows-arm64": "2.10.5" } }, - "node_modules/turbo-darwin-64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.13.4.tgz", - "integrity": "sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-darwin-arm64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.13.4.tgz", - "integrity": "sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-linux-64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.13.4.tgz", - "integrity": "sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-linux-arm64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.13.4.tgz", - "integrity": "sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-windows-64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.13.4.tgz", - "integrity": "sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/turbo-windows-arm64": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.13.4.tgz", - "integrity": "sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/type-fest": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", @@ -30154,490 +31334,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, "node_modules/vitest": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", @@ -30729,9 +31425,9 @@ } }, "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.2.0.tgz", - "integrity": "sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "dev": true, "license": "MIT" }, @@ -31209,7 +31905,449 @@ "@types/pngjs": "^6.0.5", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", - "tsx": "^4.20.6" + "tsx": "^4.23.1" + } + }, + "packages/app-tests/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/app-tests/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "packages/app-tests/node_modules/@playwright/test": { @@ -31226,6 +32364,67 @@ "node": ">=18" } }, + "packages/app-tests/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "packages/app-tests/node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "packages/assets": { "name": "@documenso/assets", "version": "0.1.0" @@ -31297,7 +32496,423 @@ }, "devDependencies": { "@documenso/tsconfig": "*", - "@types/nodemailer": "^8.0.0" + "@types/nodemailer": "^8.0.1" + } + }, + "packages/email/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/email/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "packages/email/node_modules/ansi-regex": { @@ -31342,6 +32957,47 @@ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "license": "MIT" }, + "packages/email/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "packages/email/node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -31623,8 +33279,8 @@ "pino": "^9.14.0", "pino-pretty": "^13.1.2", "playwright": "1.56.1", - "postcss": "^8.5.14", - "postcss-selector-parser": "^7.1.1", + "postcss": "^8.5.19", + "postcss-selector-parser": "^7.1.4", "posthog-js": "^1.297.2", "posthog-node": "4.18.0", "react": "^18", @@ -31686,10 +33342,513 @@ "devDependencies": { "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", - "tsx": "^4.20.6", + "tsx": "^4.23.1", "typescript": "5.6.2" } }, + "packages/prisma/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "packages/prisma/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "packages/prisma/node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "packages/signing": { "name": "@documenso/signing", "version": "0.0.0", @@ -31711,7 +33870,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.19", "autoprefixer": "^10.4.22", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "tailwindcss": "^3.4.18", "tailwindcss-animate": "^1.0.7" }, diff --git a/package.json b/package.json index 2800c7f84..984663e94 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@ts-rest/serverless": "^3.52.1", "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", + "esbuild": "^0.27.0", "husky": "^9.1.7", "inngest": "^3.54.0", "inngest-cli": "^1.17.9", @@ -78,7 +79,7 @@ "rimraf": "^6.1.2", "superjson": "^2.2.5", "syncpack": "^14.0.0-alpha.27", - "turbo": "^1.13.4", + "turbo": "^2.10.0", "vite": "^7.2.4", "vite-plugin-static-copy": "^3.1.4", "zod-openapi": "^4.2.4", @@ -104,6 +105,7 @@ "overrides": { "lodash": "4.18.1", "pdfjs-dist": "5.4.296", + "postcss": "^8.5.19", "typescript": "5.6.2", "zod": "$zod", "fumadocs-mdx": { diff --git a/packages/app-tests/package.json b/packages/app-tests/package.json index d1b80069c..7b552080c 100644 --- a/packages/app-tests/package.json +++ b/packages/app-tests/package.json @@ -18,7 +18,7 @@ "@playwright/test": "1.56.1", "@types/node": "^20", "@types/pngjs": "^6.0.5", - "tsx": "^4.20.6", + "tsx": "^4.23.1", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0" }, diff --git a/packages/email/package.json b/packages/email/package.json index 5fbc4a88c..33426f9e5 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -44,6 +44,6 @@ }, "devDependencies": { "@documenso/tsconfig": "*", - "@types/nodemailer": "^8.0.0" + "@types/nodemailer": "^8.0.1" } } diff --git a/packages/lib/package.json b/packages/lib/package.json index 450f39172..41b1f1815 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -60,8 +60,8 @@ "pino": "^9.14.0", "pino-pretty": "^13.1.2", "playwright": "1.56.1", - "postcss": "^8.5.14", - "postcss-selector-parser": "^7.1.1", + "postcss": "^8.5.19", + "postcss-selector-parser": "^7.1.4", "posthog-js": "^1.297.2", "posthog-node": "4.18.0", "react": "^18", diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 6b4f4f31a..67f2ae110 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -35,7 +35,7 @@ "devDependencies": { "dotenv": "^17.2.3", "dotenv-cli": "^11.0.0", - "tsx": "^4.20.6", + "tsx": "^4.23.1", "typescript": "5.6.2" } } diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index c405307c8..3820e782a 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -11,7 +11,7 @@ "@tailwindcss/container-queries": "^0.1.1", "@tailwindcss/typography": "^0.5.19", "autoprefixer": "^10.4.22", - "postcss": "^8.5.14", + "postcss": "^8.5.19", "tailwindcss": "^3.4.18", "tailwindcss-animate": "^1.0.7" }, diff --git a/turbo.json b/turbo.json index b417941fa..d0bd4d274 100644 --- a/turbo.json +++ b/turbo.json @@ -1,6 +1,6 @@ { "$schema": "https://turbo.build/schema.json", - "pipeline": { + "tasks": { "build": { "dependsOn": ["prebuild", "^build"], "outputs": [".next/**", "!.next/cache/**"] @@ -143,6 +143,7 @@ "GOOGLE_VERTEX_API_KEY", "CI", "NODE_ENV", + "NEXT_IGNORE_INCORRECT_LOCKFILE", "POSTGRES_URL", "DATABASE_URL", "DATABASE_URL_UNPOOLED", From 3ff7f70a7ddd7c9a451a86035dd5588ced92e976 Mon Sep 17 00:00:00 2001 From: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com> Date: Wed, 15 Jul 2026 03:42:13 +0000 Subject: [PATCH 36/41] feat: redesign api tokens settings page (#3076) --- .../dialogs/token-create-dialog.tsx | 250 +++++++++++++++++ .../dialogs/token-delete-dialog.tsx | 27 +- apps/remix/app/components/forms/token.tsx | 254 ------------------ .../t.$teamUrl+/settings.tokens.tsx | 166 +++++++----- packages/ui/primitives/alert-dialog.tsx | 2 +- packages/ui/primitives/dialog.tsx | 2 +- packages/ui/primitives/sheet.tsx | 165 ++++++------ 7 files changed, 453 insertions(+), 413 deletions(-) create mode 100644 apps/remix/app/components/dialogs/token-create-dialog.tsx delete mode 100644 apps/remix/app/components/forms/token.tsx diff --git a/apps/remix/app/components/dialogs/token-create-dialog.tsx b/apps/remix/app/components/dialogs/token-create-dialog.tsx new file mode 100644 index 000000000..0131638e2 --- /dev/null +++ b/apps/remix/app/components/dialogs/token-create-dialog.tsx @@ -0,0 +1,250 @@ +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { trpc } from '@documenso/trpc/react'; +import { ZCreateApiTokenRequestSchema } from '@documenso/trpc/server/api-token-router/create-api-token.types'; +import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import type * as DialogPrimitive from '@radix-ui/react-dialog'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { match } from 'ts-pattern'; +import type { z } from 'zod'; + +import { useCurrentTeam } from '~/providers/team'; + +const NEVER_EXPIRE = 'NEVER' as const; + +export const EXPIRATION_DATES = { + ONE_WEEK: msg`7 days`, + ONE_MONTH: msg`1 month`, + THREE_MONTHS: msg`3 months`, + SIX_MONTHS: msg`6 months`, + ONE_YEAR: msg`12 months`, + [NEVER_EXPIRE]: msg`Never`, +} as const; + +const ZCreateTokenFormSchema = ZCreateApiTokenRequestSchema.pick({ + tokenName: true, + expirationDate: true, +}); + +type TCreateTokenFormSchema = z.infer; + +export type TokenCreateDialogProps = { + trigger?: React.ReactNode; +} & Omit; + +export const TokenCreateDialog = ({ trigger, ...props }: TokenCreateDialogProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + + const team = useCurrentTeam(); + + const [open, setOpen] = useState(false); + const [createdToken, setCreatedToken] = useState(null); + + const form = useForm({ + resolver: zodResolver(ZCreateTokenFormSchema), + defaultValues: { + tokenName: '', + expirationDate: 'THREE_MONTHS', + }, + }); + + const { mutateAsync: createToken } = trpc.apiToken.create.useMutation(); + + const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenFormSchema) => { + try { + const { token } = await createToken({ + teamId: team.id, + tokenName, + expirationDate: expirationDate === NEVER_EXPIRE ? null : expirationDate, + }); + + setCreatedToken(token); + } catch (err) { + const error = AppError.parseError(err); + + const errorMessage = match(error.code) + .with(AppErrorCode.UNAUTHORIZED, () => msg`You do not have permission to create a token for this team.`) + .otherwise(() => msg`Something went wrong. Please try again later.`); + + toast({ + title: _(msg`An error occurred`), + description: _(errorMessage), + variant: 'destructive', + duration: 5000, + }); + } + }; + + useEffect(() => { + if (open) { + form.reset(); + setCreatedToken(null); + } + }, [open, form]); + + return ( + !form.formState.isSubmitting && setOpen(value)} {...props}> + e.stopPropagation()} asChild> + {trigger ?? ( + + )} + + + { + // Prevent losing the created token by accidentally clicking outside the dialog. + if (createdToken) { + event.preventDefault(); + } + }} + > + {createdToken ? ( + <> + + + Token created + + + + Copy your token now. For security reasons you will not be able to see it again. + + + +
+ +
+ toast({ title: _(msg`Token copied to clipboard`) })} + /> +
+
+ + + + + + ) : ( + <> + + + Create API token + + + + Use API tokens to authenticate with the Documenso API. + + + +
+ +
+ ( + + + Name + + + + + + + + A name to help you identify this token later. + + + + + )} + /> + + ( + + + Expires in + + + + + + + + + )} + /> + + + + + + +
+
+ + + )} +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/token-delete-dialog.tsx b/apps/remix/app/components/dialogs/token-delete-dialog.tsx index b4378ceee..0e3553a4d 100644 --- a/apps/remix/app/components/dialogs/token-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/token-delete-dialog.tsx @@ -105,7 +105,7 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe - Are you sure you want to delete this token? + Delete token @@ -139,21 +139,18 @@ export default function TokenDeleteDialog({ token, onDelete, children }: TokenDe /> -
- + - -
+
diff --git a/apps/remix/app/components/forms/token.tsx b/apps/remix/app/components/forms/token.tsx deleted file mode 100644 index 4239b0c76..000000000 --- a/apps/remix/app/components/forms/token.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { trpc } from '@documenso/trpc/react'; -import { ZCreateApiTokenRequestSchema } from '@documenso/trpc/server/api-token-router/create-api-token.types'; -import { cn } from '@documenso/ui/lib/utils'; -import { Button } from '@documenso/ui/primitives/button'; -import { Card, CardContent } from '@documenso/ui/primitives/card'; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@documenso/ui/primitives/form/form'; -import { Input } from '@documenso/ui/primitives/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@documenso/ui/primitives/select'; -import { Switch } from '@documenso/ui/primitives/switch'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import type { ApiToken } from '@prisma/client'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { match } from 'ts-pattern'; -import type { z } from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; - -export const EXPIRATION_DATES = { - ONE_WEEK: msg`7 days`, - ONE_MONTH: msg`1 month`, - THREE_MONTHS: msg`3 months`, - SIX_MONTHS: msg`6 months`, - ONE_YEAR: msg`12 months`, -} as const; - -const ZCreateTokenFormSchema = ZCreateApiTokenRequestSchema.pick({ - tokenName: true, - expirationDate: true, -}); - -type TCreateTokenFormSchema = z.infer; - -type NewlyCreatedToken = { - id: number; - token: string; -}; - -export type ApiTokenFormProps = { - className?: string; - tokens?: Pick[]; -}; - -export const ApiTokenForm = ({ className, tokens }: ApiTokenFormProps) => { - const [, copy] = useCopyToClipboard(); - - const team = useCurrentTeam(); - - const { _ } = useLingui(); - const { toast } = useToast(); - - const [newlyCreatedToken, setNewlyCreatedToken] = useState(); - const [noExpirationDate, setNoExpirationDate] = useState(false); - - const { mutateAsync: createTokenMutation } = trpc.apiToken.create.useMutation({ - onSuccess(data) { - setNewlyCreatedToken(data); - }, - }); - - const form = useForm({ - resolver: zodResolver(ZCreateTokenFormSchema), - defaultValues: { - tokenName: '', - expirationDate: '', - }, - }); - - const copyToken = async (token: string) => { - try { - const copied = await copy(token); - - if (!copied) { - throw new Error('Unable to copy the token'); - } - - toast({ - title: _(msg`Token copied to clipboard`), - description: _(msg`The token was copied to your clipboard.`), - }); - } catch (error) { - toast({ - title: _(msg`Unable to copy token`), - description: _(msg`We were unable to copy the token to your clipboard. Please try again.`), - variant: 'destructive', - }); - } - }; - - const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenFormSchema) => { - try { - await createTokenMutation({ - teamId: team.id, - tokenName, - expirationDate: noExpirationDate ? null : expirationDate, - }); - - toast({ - title: _(msg`Token created`), - description: _(msg`A new token was created successfully.`), - duration: 5000, - }); - - form.reset(); - } catch (err) { - const error = AppError.parseError(err); - - const errorMessage = match(error.code) - .with(AppErrorCode.UNAUTHORIZED, () => msg`You do not have permission to create a token for this team.`) - .otherwise(() => msg`Something went wrong. Please try again later.`); - - toast({ - title: _(msg`An error occurred`), - description: _(errorMessage), - variant: 'destructive', - duration: 5000, - }); - } - }; - - return ( -
-
- -
- ( - - - Token name - - -
- - - -
- - - Please enter a meaningful name for your token. This will help you identify it later. - - - -
- )} - /> - -
- ( - - - Token expiration date - - -
- - - -
- - -
- )} - /> - -
- - Never expire - -
- -
-
-
- - - -
- -
-
-
- - - - {newlyCreatedToken && tokens && tokens.find((token) => token.id === newlyCreatedToken.id) && ( - - - -

- - Your token was created successfully! Make sure to copy it because you won't be able to see it again! - -

- -

- {newlyCreatedToken.token} -

- - -
-
-
- )} -
-
- ); -}; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx index 40ac2aac5..f3f5bb897 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.tokens.tsx @@ -1,14 +1,18 @@ import { trpc } from '@documenso/trpc/react'; +import type { TGetApiTokensResponse } from '@documenso/trpc/server/api-token-router/get-api-tokens.types'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; +import { Badge } from '@documenso/ui/primitives/badge'; import { Button } from '@documenso/ui/primitives/button'; +import { DataTable, type DataTableColumnDef } from '@documenso/ui/primitives/data-table'; +import { Skeleton } from '@documenso/ui/primitives/skeleton'; +import { TableCell } from '@documenso/ui/primitives/table'; import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; import { TeamMemberRole } from '@prisma/client'; -import { DateTime } from 'luxon'; +import { useMemo } from 'react'; +import { TokenCreateDialog } from '~/components/dialogs/token-create-dialog'; import TokenDeleteDialog from '~/components/dialogs/token-delete-dialog'; -import { ApiTokenForm } from '~/components/forms/token'; import { SettingsHeader } from '~/components/general/settings-header'; import { useOptionalCurrentTeam } from '~/providers/team'; import { appMetaTags } from '~/utils/meta'; @@ -18,33 +22,88 @@ export function meta() { } export default function ApiTokensPage() { - const { i18n } = useLingui(); - - const { data: tokens } = trpc.apiToken.getMany.useQuery(); + const { t, i18n } = useLingui(); const team = useOptionalCurrentTeam(); + const isUnauthorized = !!team && team.currentTeamRole !== TeamMemberRole.ADMIN; + + const { + data: tokens, + isLoading, + isError, + } = trpc.apiToken.getMany.useQuery(undefined, { + enabled: !isUnauthorized, + }); + + const columns = useMemo(() => { + return [ + { + header: t`Name`, + cell: ({ row }) => {row.original.name}, + }, + { + header: t`Created`, + cell: ({ row }) => i18n.date(row.original.createdAt), + }, + { + header: t`Expires`, + cell: ({ row }) => { + if (!row.original.expires) { + return ( + + Never + + ); + } + + if (row.original.expires < new Date()) { + return ( + + Expired + + ); + } + + return i18n.date(row.original.expires); + }, + }, + { + header: t`Actions`, + cell: ({ row }) => ( + + + + ), + }, + ] satisfies DataTableColumnDef[]; + }, []); + return (
API Tokens} subtitle={ - On this page, you can create and manage API tokens. See our{' '} + Create and manage API tokens. See our{' '} - Documentation + documentation {' '} for more information. } - /> + > + {!isUnauthorized && } + - {team && team?.currentTeamRole !== TeamMemberRole.ADMIN ? ( + {isUnauthorized ? (
@@ -56,58 +115,43 @@ export default function ApiTokensPage() {
) : ( - <> - - -
- -

- Your existing tokens -

- - {tokens && tokens.length === 0 && ( -
-

- Your tokens will be shown here once you create them. + +

+ You have no API tokens yet. Your tokens will be shown here once you create them.

- )} - - {tokens && tokens.length > 0 && ( -
- {tokens.map((token) => ( -
-
-
-
{token.name}
- -

- Created on {i18n.date(token.createdAt, DateTime.DATETIME_FULL)} -

- {token.expires ? ( -

- Expires on {i18n.date(token.expires, DateTime.DATETIME_FULL)} -

- ) : ( -

- Token doesn't have an expiration date -

- )} -
- -
- - - -
-
-
- ))} -
- )} - + } + skeleton={{ + enable: isLoading, + rows: 3, + component: ( + <> + + + + + + + + + + + + + + ), + }} + /> )}
); diff --git a/packages/ui/primitives/alert-dialog.tsx b/packages/ui/primitives/alert-dialog.tsx index 0bd424445..77c1cfaa2 100644 --- a/packages/ui/primitives/alert-dialog.tsx +++ b/packages/ui/primitives/alert-dialog.tsx @@ -41,7 +41,7 @@ const AlertDialogContent = React.forwardRef< , From 40472bc26c1a78f4c77507e967253a0b2a68d3e7 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Thu, 16 Jul 2026 12:21:43 +1000 Subject: [PATCH 37/41] fix: react hydration errors (#3099) Move PostHog init out of the React tree so the client tree matches the server render (mismatched useIds could abort hydration, leaving dead event handlers). Also expose the CSP nonce so Radix scroll-lock styles aren't blocked. --- apps/remix/app/entry.client.tsx | 35 +++++++++++++++++++-------------- apps/remix/app/root.tsx | 12 +++++++++-- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/apps/remix/app/entry.client.tsx b/apps/remix/app/entry.client.tsx index 86e949d7a..0ffb3602e 100644 --- a/apps/remix/app/entry.client.tsx +++ b/apps/remix/app/entry.client.tsx @@ -3,27 +3,32 @@ import { dynamicActivate } from '@documenso/lib/utils/i18n'; import { i18n } from '@lingui/core'; import { detect, fromHtmlTag } from '@lingui/detect-locale'; import { I18nProvider } from '@lingui/react'; -import { StrictMode, startTransition, useEffect } from 'react'; +import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; import './utils/polyfills/promise-with-resolvers'; -function PosthogInit() { +/** + * Initialised imperatively (not as a component inside `hydrateRoot`) because + * rendering extra client-only siblings changes the React tree structure + * relative to the server render in `entry.server.tsx`. That shifts every + * `useId` value (used by Radix for `id`/`htmlFor`/`aria-*`), causing hydration + * mismatches which can abort hydration entirely when the user interacts with + * the page early, leaving dead event handlers (broken dropdowns, native form + * submits). + */ +function initPosthog() { const postHogConfig = extractPostHogConfig(); - useEffect(() => { - if (postHogConfig) { - void import('posthog-js').then(({ default: posthog }) => { - posthog.init(postHogConfig.key, { - api_host: postHogConfig.host, - capture_exceptions: true, - }); + if (postHogConfig) { + void import('posthog-js').then(({ default: posthog }) => { + posthog.init(postHogConfig.key, { + api_host: postHogConfig.host, + capture_exceptions: true, }); - } - }, []); - - return null; + }); + } } async function main() { @@ -38,11 +43,11 @@ async function main() { - - , ); }); + + void initPosthog(); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 096b1504e..a7c29b4be 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -119,7 +119,11 @@ export function LayoutContent({ children }: { children: React.ReactNode }) { const isRecipientRoute = matches.some((m) => m.id?.startsWith('routes/_recipient+')); return ( - + // `suppressHydrationWarning` because `remix-themes` intentionally mutates + // `data-theme`/`class` on before hydration (PreventFlashOnWrongTheme), + // so the server-rendered attributes never match the client render when the + // theme is resolved from the system preference. Attribute-only, one level deep. + @@ -173,7 +177,11 @@ export function LayoutContent({ children }: { children: React.ReactNode }) {