From 1ae804fd89b9dd2ae3e4ffc44243b72f9b3f2e05 Mon Sep 17 00:00:00 2001 From: Mudit Lal Date: Wed, 19 Aug 2026 20:35:32 +0530 Subject: [PATCH] chore: sync upstream documenso/documenso @ 7533016 (v2.17.0) Squash-import of upstream/main (120 commits, v2.11 -> v2.17.0). Primary motivation: the previously deployed commit predates upstream 583e35c7 ("fix: ensures new expire on setSessionCookie", #2708). Before that fix, `sessionCookieOptions` carried expires: new Date(Date.now() + AUTH_SESSION_LIFETIME) evaluated once at module import, and `setSessionCookie` passed those options through unchanged with no `maxAge`. Every session cookie the process ever issued therefore expired at (process start + 30 days) -- frozen at 2026-07-04T18:54:19Z for the deployment running since 2026-06-04. Browsers discarded the cookie on arrival, so all logins (Google OAuth and email/password alike) silently failed from that date. Also imported upstream's deletions, which the 2026-06-04 sync missed: packages/eslint-config/, packages/prettier-config/, prettier.config.cjs, .prettierignore, .eslintrc.cjs, .eslintignore, tsconfig.eslint.json, packages/lib/server-only/document/send-completed-email.ts and the stale embedding/authoring docs. DEVALOK_FORK_NOTES.md now documents the read-tree import method so future syncs propagate deletions. Fork CI guards re-applied to 13 workflows. Upstream deleted issue-assignee-check.yml and pr-review-reminder.yml, so those rows are dropped from the guarded list. Per-job audit confirms the only intentionally open jobs remain ci.yml/build_docker and codeql-analysis.yml/analyze. Pending Prisma migrations (all additive, no drops): 20260525103410_add_signature_level_and_csc_tables 20260604143030_add_email_transports 20260616120000_add_cancelled_document_status 20260622120000_add_recipient_reminder_count Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DYNE3dT1E7bGHk8Y8joXMm --- ...arp-gold-mountain-custom-brand-logo-url.md | 122 + ...wave-rejected-expired-recipient-filters.md | 146 + .agents/skills/create-justification/SKILL.md | 56 - .agents/skills/create-plan/SKILL.md | 56 - .agents/skills/create-scratch/SKILL.md | 56 - .env.example | 26 +- .eslintignore | 8 - .eslintrc.cjs | 16 - .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/ci.yml | 2 + .github/workflows/codeql-analysis.yml | 1 + .github/workflows/deploy.yml | 1 + .github/workflows/first-interaction.yml | 8 +- .github/workflows/issue-assignee-check.yml | 62 - .github/workflows/issue-labeler.yml | 1 + .github/workflows/issue-opened.yml | 1 + .github/workflows/pr-labeler.yml | 1 + .github/workflows/pr-review-reminder.yml | 63 - .github/workflows/publish.yml | 2 + .github/workflows/semantic-pull-requests.yml | 28 +- .github/workflows/stale.yml | 3 +- .github/workflows/translations-force-pull.yml | 1 + .github/workflows/translations-pull.yml | 1 + .github/workflows/translations-upload.yml | 1 + .gitpod.yml | 3 +- .prettierignore | 20 - .well-known/security.txt | 15 +- ARCHITECTURE.md | 2 +- CONTRIBUTING.md | 26 +- DEVALOK_FORK_NOTES.md | 50 +- README.md | 36 +- SECURITY.md | 38 + SIGNING.md | 70 +- apps/docs/README.md | 47 +- .../content/docs/developers/api/documents.mdx | 235 +- .../content/docs/developers/api/index.mdx | 2 + .../content/docs/developers/api/meta.json | 1 + .../developers/api/migrate-to-envelopes.mdx | 249 + .../docs/developers/api/rate-limits.mdx | 85 +- .../content/docs/developers/api/templates.mdx | 2 + .../docs/developers/api/versioning.mdx | 11 + .../developers/embedding/authoring/index.mdx | 56 - .../developers/embedding/authoring/meta.json | 4 - .../developers/embedding/authoring/v1.mdx | 300 - .../developers/embedding/authoring/v2.mdx | 344 - .../developers/examples/common-workflows.mdx | 15 +- .../docs/developers/examples/index.mdx | 2 + .../getting-started/authentication.mdx | 2 + .../getting-started/first-api-call.mdx | 4 +- .../docs/developers/getting-started/index.mdx | 2 + apps/docs/content/docs/developers/index.mdx | 2 + .../developers/local-development/index.mdx | 15 +- .../docs/developers/webhooks/events.mdx | 115 +- .../docs/developers/webhooks/index.mdx | 6 +- .../docs/developers/webhooks/setup.mdx | 39 +- .../docs/developers/webhooks/verification.mdx | 1 + .../docs/policies/enterprise-edition.mdx | 5 +- apps/docs/content/docs/policies/fair-use.mdx | 7 +- apps/docs/content/docs/policies/meta.json | 1 + .../content/docs/policies/verify-email.mdx | 68 + .../docs/self-hosting/configuration/email.mdx | 4 +- .../configuration/environment.mdx | 107 +- .../docs/self-hosting/configuration/index.mdx | 5 + .../self-hosting/configuration/license.mdx | 107 + .../docs/self-hosting/configuration/meta.json | 2 + .../configuration/organisation-limits.mdx | 111 + .../signing-certificate/csc-qes.mdx | 213 + .../signing-certificate/index.mdx | 19 +- .../signing-certificate/meta.json | 2 +- .../deployment/docker-compose.mdx | 15 +- .../docs/self-hosting/deployment/docker.mdx | 6 + .../self-hosting/deployment/kubernetes.mdx | 2 +- .../docs/self-hosting/deployment/manual.mdx | 4 +- .../docs/self-hosting/deployment/railway.mdx | 8 +- .../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 | 10 +- .../self-hosting/maintenance/upgrades.mdx | 20 +- .../users/getting-started/create-account.mdx | 6 +- .../docs/users/settings/delete-account.mdx | 31 +- apps/docs/package.json | 11 +- .../documenso-registration-form.webp | Bin 0 -> 36930 bytes apps/docs/src/app/global.css | 2 +- .../src/components/mdx/envelope-warning.tsx | 19 + apps/docs/src/mdx-components.tsx | 2 + apps/openpage-api/package.json | 4 +- apps/remix/Dockerfile | 22 - apps/remix/README.md | 100 +- .../branding-preferences-reset-dialog.tsx | 119 + .../dialogs/claim-update-dialog.tsx | 32 +- .../document-move-to-folder-dialog.tsx | 243 - .../document-preferences-reset-dialog.tsx | 122 + .../dialogs/document-resend-dialog.tsx | 198 - .../dialogs/email-transport-create-dialog.tsx | 95 + .../dialogs/email-transport-delete-dialog.tsx | 114 + .../email-transport-send-test-dialog.tsx | 126 + .../dialogs/email-transport-update-dialog.tsx | 104 + .../dialogs/envelope-cancel-dialog.tsx | 134 + .../dialogs/envelope-delete-dialog.tsx | 2 +- .../dialogs/envelope-distribute-dialog.tsx | 61 +- .../dialogs/envelope-duplicate-dialog.tsx | 78 +- .../dialogs/envelope-redistribute-dialog.tsx | 36 +- .../dialogs/envelopes-bulk-cancel-dialog.tsx | 159 + .../envelopes-bulk-download-dialog.tsx | 377 + .../dialogs/envelopes-bulk-move-dialog.tsx | 5 +- .../dialogs/folder-create-dialog.tsx | 5 +- .../dialogs/folder-delete-dialog.tsx | 2 +- .../dialogs/folder-update-dialog.tsx | 6 +- .../dialogs/organisation-create-dialog.tsx | 6 +- ...rganisation-email-domain-delete-dialog.tsx | 2 +- .../organisation-member-invite-dialog.tsx | 2 +- .../dialogs/passkey-create-dialog.tsx | 4 +- .../dialogs/team-email-delete-dialog.tsx | 35 +- .../dialogs/team-email-update-dialog.tsx | 17 +- .../dialogs/team-member-delete-dialog.tsx | 31 +- .../template-move-to-folder-dialog.tsx | 232 - .../dialogs/template-use-dialog.tsx | 21 +- .../dialogs/token-create-dialog.tsx | 250 + .../dialogs/token-delete-dialog.tsx | 31 +- .../dialogs/webhook-delete-dialog.tsx | 2 +- .../embed/authoring/configure-fields-view.tsx | 2 +- .../embed-direct-template-client-page.tsx | 9 +- .../embed/embed-document-signing-page-v1.tsx | 9 +- .../multi-sign-document-signing-view.tsx | 8 +- .../forms/branding-preferences-form.tsx | 226 +- .../forms/certificate-preferences-form.tsx | 169 + .../forms/document-preferences-form.tsx | 502 +- .../forms/email-preferences-form.tsx | 144 +- .../components/forms/email-transport-form.tsx | 318 + .../components/forms/form-sticky-save-bar.tsx | 146 + .../components/forms/inheritable-field.tsx | 51 + .../forms/organisation-update-form.tsx | 41 +- apps/remix/app/components/forms/profile.tsx | 2 +- .../forms/reminder-preferences-form.tsx | 134 + apps/remix/app/components/forms/signin.tsx | 129 +- apps/remix/app/components/forms/signup.tsx | 4 +- .../forms/subscription-claim-form.tsx | 41 + .../app/components/forms/team-update-form.tsx | 41 +- apps/remix/app/components/forms/token.tsx | 254 - .../general/admin-global-settings-section.tsx | 113 +- .../components/general/admin-license-card.tsx | 24 +- .../components/general/app-command-menu.tsx | 971 +- .../general/app-command-menu.types.ts | 36 + .../app/components/general/app-header.tsx | 26 +- .../components/general/app-nav-desktop.tsx | 5 +- .../app/components/general/app-nav-mobile.tsx | 5 +- .../app/components/general/billing-plans.tsx | 20 +- .../app/components/general/claim-account.tsx | 4 +- .../components/general/claim-limit-fields.tsx | 95 +- .../direct-template-invalid-page.tsx | 23 + .../direct-template/direct-template-page.tsx | 14 +- .../csc-recipient-blocked-page.tsx | 68 + ...csc-recipient-signing-in-progress-page.tsx | 105 + .../document-signing-attachments-popover.tsx | 3 +- .../document-signing-auth-account.tsx | 5 +- .../document-signing-auth-page.tsx | 5 +- .../document-signing-complete-dialog.tsx | 18 +- .../document-signing-form.tsx | 10 +- .../document-signing-page-view-v1.tsx | 15 +- .../document-signing-radio-field.tsx | 10 +- .../document-signing-reject-dialog.tsx | 5 +- .../document/document-attachments-popover.tsx | 5 +- .../document-audit-log-download-button.tsx | 6 +- .../document-certificate-download-button.tsx | 6 +- .../general/document/document-edit-form.tsx | 10 +- .../document/document-page-view-button.tsx | 4 +- .../document/document-page-view-dropdown.tsx | 26 +- .../general/document/document-search.tsx | 34 +- .../general/document/document-status.tsx | 14 +- .../document-upload-button-legacy.tsx | 29 +- .../embedded-editor-attachment-popover.tsx | 5 +- .../envelope-editor-fields-drag-drop.tsx | 2 +- .../envelope-editor-fields-page-renderer.tsx | 339 +- .../envelope-editor-fields-page.tsx | 79 +- .../envelope-editor-header.tsx | 5 + ...e-editor-invalid-direct-template-alert.tsx | 55 + .../envelope-editor-preview-page.tsx | 3 + .../envelope-editor-recipient-form.tsx | 130 +- .../envelope-editor-settings-dialog.tsx | 52 +- .../envelope-editor-upload-page.tsx | 4 + .../envelope-signer-header.tsx | 30 +- .../envelope-signer-page-renderer.tsx | 31 +- .../envelope-signing-complete-dialog.tsx | 58 +- .../envelope/envelope-drop-zone-wrapper.tsx | 28 +- .../envelope/envelope-upload-button.tsx | 26 +- .../app/components/general/filter-pill.tsx | 186 + .../app/components/general/menu-switcher.tsx | 104 - .../components/general/org-menu-switcher.tsx | 52 +- .../general/organisation-usage-panel.tsx | 308 +- .../organisation-usage-reset-button.tsx | 2 + .../organisation-billing-banner.tsx | 88 +- .../organisation-billing-portal-button.tsx | 8 +- .../organisation-quota-banner.tsx | 168 + .../general/pdf-viewer/pdf-viewer.tsx | 2 +- .../general/rate-limit-array-input.tsx | 167 +- .../components/general/settings-header.tsx | 6 +- .../general/settings-nav-desktop.tsx | 137 - .../general/settings-nav-mobile.tsx | 144 - .../general/settings-org-switcher.tsx | 200 + .../general/settings-scope-breadcrumb.tsx | 58 + .../general/settings-team-switcher.tsx | 166 + .../settings-upsell/branding-upsell.tsx | 195 + .../settings-upsell/email-domains-upsell.tsx | 213 + .../general/settings-upsell/motion.ts | 9 + .../settings-upsell/settings-upsell-card.tsx | 118 + .../settings-upsell/sso-portal-upsell.tsx | 260 + .../settings-upsell/use-timed-cycle.ts | 47 + .../general/teams/team-email-dropdown.tsx | 94 - .../general/unified-settings-layout.tsx | 211 + .../unified-settings-sidebar-mobile.tsx | 165 + .../general/unified-settings-sidebar.tsx | 275 + .../general/use-admin-search-categories.ts | 144 + .../tables/admin-email-transports-table.tsx | 179 + .../tables/documents-table-action-button.tsx | 4 +- .../documents-table-action-dropdown.tsx | 61 +- .../tables/documents-table-empty-state.tsx | 17 +- .../tables/documents-table-period-filter.tsx | 40 + .../tables/documents-table-sender-filter.tsx | 64 +- .../tables/documents-table-status-filter.tsx | 98 + .../app/components/tables/documents-table.tsx | 4 +- .../envelopes-table-bulk-action-bar.tsx | 107 +- .../app/components/tables/inbox-table.tsx | 6 +- .../tables/organisation-teams-table.tsx | 2 +- ...ettings-security-passkey-table-actions.tsx | 3 +- .../components/tables/team-members-table.tsx | 56 +- .../templates-table-action-dropdown.tsx | 16 +- .../user-billing-organisations-table.tsx | 26 +- .../tables/user-organisations-table.tsx | 49 +- apps/remix/app/entry.client.tsx | 54 +- apps/remix/app/root.tsx | 29 +- .../app/routes/_authenticated+/_layout.tsx | 31 +- .../routes/_authenticated+/admin+/_layout.tsx | 11 + .../routes/_authenticated+/admin+/claims.tsx | 2 +- .../admin+/email-transports._index.tsx | 59 + .../admin+/organisations.$id.tsx | 360 +- .../_authenticated+/admin+/teams.$id.tsx | 6 +- .../app/routes/_authenticated+/dashboard.tsx | 4 +- .../_authenticated+/o.$orgUrl._index.tsx | 4 +- .../_authenticated+/o.$orgUrl._layout.tsx | 12 +- .../o.$orgUrl.settings._index.tsx | 11 - .../o.$orgUrl.settings._layout.tsx | 196 +- .../o.$orgUrl.settings.billing.tsx | 43 +- .../o.$orgUrl.settings.branding.tsx | 60 +- .../o.$orgUrl.settings.certificates.tsx | 80 + .../o.$orgUrl.settings.document.tsx | 39 +- .../o.$orgUrl.settings.email-domains.$id.tsx | 20 +- ....$orgUrl.settings.email-domains._index.tsx | 19 +- .../o.$orgUrl.settings.email.tsx | 7 +- .../o.$orgUrl.settings.general.tsx | 32 +- .../o.$orgUrl.settings.groups.$id.tsx | 11 +- .../o.$orgUrl.settings.groups._index.tsx | 1 + .../o.$orgUrl.settings.members.tsx | 6 +- .../o.$orgUrl.settings.reminders.tsx | 76 + .../o.$orgUrl.settings.sso.tsx | 34 +- .../o.$orgUrl.settings.teams.tsx | 2 +- .../_dynamic_personal_routes+/_layout.tsx | 54 - .../billing-personal.tsx | 5 - .../_dynamic_personal_routes+/branding.tsx | 5 - .../_dynamic_personal_routes+/document.tsx | 5 - .../_dynamic_personal_routes+/email.tsx | 5 - .../public-profile.tsx | 5 - .../_dynamic_personal_routes+/tokens.tsx | 5 - .../webhooks.$id._index.tsx | 5 - .../webhooks._index.tsx | 5 - .../_authenticated+/settings+/_index.tsx | 5 - .../_authenticated+/settings+/_layout.tsx | 53 +- .../_authenticated+/settings+/billing.tsx | 1 + .../settings+/organisations.tsx | 1 + .../_authenticated+/settings+/profile.tsx | 2 - .../settings+/security._index.tsx | 8 +- .../settings+/security.activity.tsx | 2 +- .../settings+/security.passkeys.tsx | 2 +- .../_authenticated+/t.$teamUrl+/_layout.tsx | 15 +- .../t.$teamUrl+/documents.$id._index.tsx | 1 + .../t.$teamUrl+/documents.$id.logs.tsx | 13 +- .../t.$teamUrl+/documents._index.tsx | 277 +- .../t.$teamUrl+/settings._index.tsx | 162 - .../t.$teamUrl+/settings._layout.tsx | 135 +- .../t.$teamUrl+/settings.branding.tsx | 114 +- .../t.$teamUrl+/settings.certificates.tsx | 76 + .../t.$teamUrl+/settings.document.tsx | 33 +- .../t.$teamUrl+/settings.email.tsx | 13 +- .../t.$teamUrl+/settings.general.tsx | 281 + .../t.$teamUrl+/settings.groups.tsx | 2 +- .../t.$teamUrl+/settings.members.tsx | 2 +- .../t.$teamUrl+/settings.public-profile.tsx | 5 +- .../t.$teamUrl+/settings.reminders.tsx | 76 + .../t.$teamUrl+/settings.tokens.tsx | 167 +- .../settings.webhooks.$id._index.tsx | 1 + .../t.$teamUrl+/settings.webhooks._index.tsx | 1 + .../t.$teamUrl+/templates._index.tsx | 12 +- apps/remix/app/routes/_index.tsx | 3 +- apps/remix/app/routes/_profile+/p.$url.tsx | 19 +- .../routes/_recipient+/d.$token+/_index.tsx | 28 + .../_recipient+/sign.$token+/_index.tsx | 108 +- .../_recipient+/sign.$token+/waiting.tsx | 2 +- .../_unauthenticated+/forgot-password.tsx | 11 +- .../organisation.decline.$token.tsx | 95 +- .../organisation.invite.$token.tsx | 325 +- .../organisation.sso.confirmation.$token.tsx | 2 +- .../reset-password.$token.tsx | 5 + .../reset-password._index.tsx | 11 +- .../app/routes/_unauthenticated+/signin.tsx | 56 +- .../team.verify.email.$token.tsx | 203 +- apps/remix/app/routes/api+/preferred-team.tsx | 21 + apps/remix/app/routes/embed+/playground.tsx | 5 +- .../embed+/v1+/authoring+/document.create.tsx | 18 +- .../embed+/v1+/authoring+/template.create.tsx | 18 +- .../v2+/authoring+/envelope.edit.$id.tsx | 4 +- .../app/utils/documents-search-params.ts | 19 + apps/remix/app/utils/toast-error-messages.ts | 150 + apps/remix/package.json | 24 +- apps/remix/public/.well-known/security.txt | 15 +- apps/remix/public/site.webmanifest | 4 +- apps/remix/react-router.config.ts | 5 + .../server/api/ai/detect-fields.client.ts | 3 +- .../server/api/ai/detect-recipients.client.ts | 3 +- apps/remix/server/api/download/download.ts | 248 +- .../server/api/download/download.types.ts | 14 + apps/remix/server/api/files/files.helpers.ts | 25 + apps/remix/server/api/files/files.ts | 26 +- apps/remix/server/api/files/files.types.ts | 13 - apps/remix/server/main.js | 13 + apps/remix/server/middleware.ts | 5 +- apps/remix/server/redirects.ts | 21 +- apps/remix/server/router.ts | 9 +- apps/remix/server/trpc/hono-trpc-open-api.ts | 6 +- apps/remix/server/trpc/hono-trpc-remix.ts | 7 +- apps/remix/vite.config.ts | 6 +- docker/Dockerfile | 13 +- docker/development/compose.yml | 2 +- docker/production/compose.yml | 8 +- docker/testing/compose.yml | 2 +- package-lock.json | 25765 +++++++++------- package.json | 61 +- packages/api/v1/implementation.ts | 9 +- packages/api/v1/openapi.ts | 2 +- .../email-transport-claims.spec.ts | 268 + .../email-transport-crud.spec.ts | 284 + .../app-tests/e2e/admin/global-search.spec.ts | 439 + .../update-organisation-member-role.spec.ts | 8 +- .../e2e/api/trpc/admin/admin-search.spec.ts | 249 + .../e2e/api/v1/document-sending.spec.ts | 202 +- .../api/v1/organisation-rate-limits.spec.ts | 6 +- .../e2e/api/v2/find-documents.spec.ts | 312 +- .../e2e/api/v2/find-envelopes.spec.ts | 117 + .../api/v2/organisation-rate-limits.spec.ts | 4 +- .../api/v2/redistribute-send-status.spec.ts | 64 + .../v2/reject-recipient-on-behalf-of.spec.ts | 260 + .../api-access-envelope-cancel.spec.ts | 242 + ...api-access-envelope-cert-audit-log.spec.ts | 284 + .../api-access-file-download.spec.ts | 118 + .../api-access-file-upload.spec.ts | 62 + .../e2e/branding-logo-optimise.spec.ts | 37 + .../e2e/branding-logo-upload.spec.ts | 225 + .../e2e/command-menu/document-search.spec.ts | 15 +- .../next-recipient-dictation.spec.ts | 227 +- .../document-flow/stepper-component.spec.ts | 205 +- .../documents/bulk-document-actions.spec.ts | 304 +- .../e2e/documents/cancel-documents.spec.ts | 339 + .../e2e/documents/delete-documents.spec.ts | 44 +- .../document-visibility-access.spec.ts | 89 + .../e2e/documents/find-documents.spec.ts | 195 +- .../attachment-url-validation.spec.ts | 70 + .../attachment-visibility-access.spec.ts | 121 + .../envelope-actions.spec.ts | 90 + ...nvelope-direct-template-validation.spec.ts | 76 + .../envelope-fields.spec.ts | 342 +- .../envelope-recipient-autosave-race.spec.ts | 199 + .../envelope-recipient-cc-order.spec.ts | 155 + .../envelope-settings.spec.ts | 69 +- .../envelope-expiration-send.spec.ts | 2 +- .../envelope-expiration-settings.spec.ts | 26 +- .../envelope-expiration-signing.spec.ts | 83 +- .../include-document-certificate.spec.ts | 50 +- .../app-tests/e2e/fixtures/command-menu.ts | 18 + packages/app-tests/e2e/fixtures/documents.ts | 111 +- packages/app-tests/e2e/fixtures/konva.ts | 32 + .../organisations/manage-organisation.spec.ts | 2 +- .../organisation-permission-hierarchy.spec.ts | 406 + .../organisation-quota-banner.spec.ts | 169 + .../organisation-team-preferences.spec.ts | 40 +- .../public-profiles/public-profiles.spec.ts | 43 + .../recipient-visibility-access.spec.ts | 72 + .../settings/preferred-team-cookie.spec.ts | 82 + .../e2e/settings/unified-settings.spec.ts | 554 + .../app-tests/e2e/signing-branding.spec.ts | 179 + .../e2e/teams/default-recipients.spec.ts | 2 +- .../app-tests/e2e/teams/manage-team.spec.ts | 5 +- .../e2e/teams/team-documents.spec.ts | 70 +- .../app-tests/e2e/teams/team-email.spec.ts | 35 +- .../app-tests/e2e/teams/team-groups.spec.ts | 163 + .../app-tests/e2e/teams/team-members.spec.ts | 105 + .../e2e/teams/team-profile-access.spec.ts | 53 + .../e2e/teams/team-settings-save-bar.spec.ts | 85 + .../e2e/teams/team-signature-settings.spec.ts | 4 +- .../templates/bulk-template-actions.spec.ts | 34 +- .../e2e/templates/direct-templates.spec.ts | 125 +- .../e2e/templates/template-use-dialog.spec.ts | 101 + .../app-tests/e2e/user/delete-account.spec.ts | 384 +- .../webhooks/webhook-secret-access.spec.ts | 74 + packages/app-tests/package.json | 4 +- packages/auth/client/index.ts | 6 +- .../auth/server/lib/errors/error-codes.ts | 1 + .../server/lib/session/session-cookies.ts | 6 +- packages/auth/server/lib/utils/cookies.ts | 5 +- .../lib/utils/handle-oauth-callback-url.ts | 17 +- .../handle-oauth-organisation-callback-url.ts | 3 +- packages/auth/server/lib/utils/redirect.ts | 35 +- packages/auth/server/routes/email-password.ts | 25 + packages/auth/server/types/email-password.ts | 2 +- packages/ee/package.json | 4 +- ...isation-account-link-confirmation-email.ts | 11 + packages/ee/server-only/limits/client.ts | 5 +- packages/ee/server-only/limits/server.ts | 10 + .../signing/csc/algorithm-resolver.ts | 347 + .../ee/server-only/signing/csc/cert-chain.ts | 122 + .../ee/server-only/signing/csc/ciphers.ts | 51 + .../signing/csc/client/credentials.ts | 122 + .../ee/server-only/signing/csc/client/http.ts | 170 + .../server-only/signing/csc/client/index.ts | 32 + .../ee/server-only/signing/csc/client/info.ts | 42 + .../server-only/signing/csc/client/oauth.ts | 321 + .../signing/csc/client/signatures.ts | 111 + .../server-only/signing/csc/client/types.ts | 179 + .../csc/cookies/blocking-error-cookie.ts | 120 + .../signing/csc/cookies/oauth-flow-cookie.ts | 85 + .../signing/csc/cookies/sad-session-cookie.ts | 61 + .../csc/cookies/service-session-cookie.ts | 65 + .../server-only/signing/csc/cookies/shared.ts | 46 + .../ee/server-only/signing/csc/credential.ts | 184 + .../signing/csc/execute-tsp-sign.ts | 548 + .../signing/csc/finalize-tsp-completion.ts | 130 + .../server-only/signing/csc/hono/context.ts | 16 + .../ee/server-only/signing/csc/hono/index.ts | 63 + .../signing/csc/hono/oauth-authorize.ts | 154 + .../signing/csc/hono/oauth-callback.ts | 303 + .../signing/csc/materialize-anchors.ts | 230 + .../ee/server-only/signing/csc/pdf-names.ts | 23 + .../signing/csc/prepare-recipient-signing.ts | 248 + .../server-only/signing/csc/render-overlay.ts | 162 + .../server-only/signing/csc/sign-session.ts | 181 + .../signing/csc/signers/capture-signer.ts | 123 + .../signing/csc/signers/fifo-signer.ts | 57 + .../ee/server-only/signing/csc/transport.ts | 153 + .../server-only/signing/csc/tsa-resolver.ts | 105 + .../signing/csc/tsp-timestamp-authority.ts | 82 + .../stripe/create-checkout-session.ts | 12 +- .../sync-stripe-customer-subscription.ts | 297 + .../update-subscription-item-quantity.ts | 137 +- .../ee/server-only/stripe/webhook/handler.ts | 115 +- .../stripe/webhook/on-subscription-created.ts | 214 - .../stripe/webhook/on-subscription-deleted.ts | 90 - .../stripe/webhook/on-subscription-updated.ts | 194 - packages/email/components.ts | 36 +- packages/email/package.json | 31 +- packages/email/preview/.gitignore | 2 + packages/email/preview/app/app.css | 9 + .../preview/app/components/playground.tsx | 337 + .../preview/app/components/prop-fields.tsx | 113 + packages/email/preview/app/entry.client.tsx | 12 + packages/email/preview/app/entry.server.tsx | 56 + packages/email/preview/app/lib/templates.tsx | 407 + packages/email/preview/app/lib/viewports.ts | 10 + packages/email/preview/app/root.tsx | 30 + packages/email/preview/app/routes.ts | 7 + packages/email/preview/app/routes/$slug.tsx | 35 + packages/email/preview/app/routes/_index.tsx | 13 + .../email/preview/app/routes/api.render.tsx | 61 + packages/email/preview/postcss.config.cjs | 6 + packages/email/preview/react-router.config.ts | 6 + packages/email/preview/tailwind.config.cjs | 24 + packages/email/preview/tsconfig.json | 30 + packages/email/preview/vite.config.ts | 72 + packages/email/providers/branding.tsx | 2 + packages/email/render.tsx | 57 +- .../template-access-auth-2fa.tsx | 14 +- .../template-admin-user-created.tsx | 14 +- .../template-branding-logo.tsx | 43 + .../template-confirmation-email.tsx | 8 +- .../template-custom-message-body.tsx | 2 +- .../template-document-cancel.tsx | 6 +- .../template-document-completed.tsx | 16 +- .../template-document-invite.tsx | 6 +- .../template-document-pending.tsx | 8 +- .../template-document-recipient-signed.tsx | 12 +- .../template-document-rejected.tsx | 6 +- .../template-document-rejection-confirmed.tsx | 4 +- .../template-document-reminder.tsx | 8 +- .../template-document-self-signed.tsx | 30 +- .../template-document-super-delete.tsx | 10 +- .../template-components/template-footer.tsx | 28 +- .../template-forgot-password.tsx | 6 +- .../template-components/template-image.tsx | 2 +- .../template-recipient-expired.tsx | 6 +- .../template-reset-password.tsx | 6 +- packages/email/templates/access-auth-2fa.tsx | 23 +- .../email/templates/admin-user-created.tsx | 7 +- .../email/templates/bulk-send-complete.tsx | 13 +- packages/email/templates/confirm-email.tsx | 22 +- .../email/templates/confirm-team-email.tsx | 29 +- packages/email/templates/document-cancel.tsx | 22 +- .../email/templates/document-completed.tsx | 23 +- .../document-created-from-direct-template.tsx | 29 +- packages/email/templates/document-invite.tsx | 26 +- packages/email/templates/document-pending.tsx | 23 +- .../templates/document-recipient-signed.tsx | 23 +- .../email/templates/document-rejected.tsx | 22 +- .../document-rejection-confirmed.tsx | 22 +- .../email/templates/document-reminder.tsx | 24 +- .../email/templates/document-self-signed.tsx | 23 +- .../email/templates/document-super-delete.tsx | 22 +- packages/email/templates/forgot-password.tsx | 22 +- ...organisation-account-link-confirmation.tsx | 26 +- .../email/templates/organisation-delete.tsx | 25 +- .../email/templates/organisation-invite.tsx | 31 +- .../email/templates/organisation-join.tsx | 25 +- .../email/templates/organisation-leave.tsx | 25 +- .../templates/organisation-limit-alert.tsx | 152 + .../templates/organisation-limit-exceeded.tsx | 115 - .../email/templates/recipient-expired.tsx | 22 +- .../recipient-removed-from-document.tsx | 24 +- packages/email/templates/reset-password.tsx | 30 +- packages/email/templates/team-delete.tsx | 25 +- .../email/templates/team-email-removed.tsx | 25 +- packages/email/transports/build-transport.ts | 51 + packages/email/transports/mailchannels.ts | 5 + .../email/transports/normalize-headers.ts | 55 + packages/email/tsconfig.json | 2 +- packages/email/utils/branding-url.ts | 21 + packages/eslint-config/index.cjs | 81 - packages/eslint-config/package.json | 19 - packages/eslint-config/tsconfig.json | 9 - packages/lib/client-only/cookies.ts | 17 + packages/lib/client-only/create-zip-writer.ts | 178 + packages/lib/client-only/download-pdf.ts | 25 +- .../lib/client-only/hooks/use-autosave.ts | 2 +- .../hooks/use-child-route-flags.ts | 55 + .../hooks/use-editor-recipients.ts | 56 +- .../hooks/use-envelope-autosave.ts | 116 +- .../providers/envelope-editor-provider.tsx | 111 +- .../providers/envelope-render-provider.tsx | 2 +- packages/lib/constants/app.ts | 114 +- packages/lib/constants/auth.ts | 42 +- packages/lib/constants/branding.ts | 10 + packages/lib/constants/cookies.ts | 1 + packages/lib/constants/document.ts | 3 + packages/lib/constants/envelope-reminder.ts | 19 +- packages/lib/constants/pdf.test.ts | 44 + packages/lib/constants/pdf.ts | 14 + packages/lib/errors/app-error.ts | 94 +- packages/lib/jobs/client.ts | 14 +- packages/lib/jobs/client/bullmq.ts | 10 +- packages/lib/jobs/client/local.ts | 10 +- .../send-document-cancelled-emails.handler.ts | 9 +- .../send-document-completed-emails.handler.ts | 11 +- ...ated-from-direct-template-email.handler.ts | 5 +- .../send-document-deleted-emails.handler.ts | 70 + .../emails/send-document-deleted-emails.ts | 52 + .../send-document-pending-email.handler.ts} | 47 +- .../emails/send-document-pending-email.ts | 30 + ...-organisation-limit-alert-email.handler.ts | 142 + .../send-organisation-limit-alert-email.ts | 34 + ...ganisation-limit-exceeded-email.handler.ts | 98 - .../send-organisation-limit-exceeded-email.ts | 34 - ...rganisation-member-joined-email.handler.ts | 5 +- ...-organisation-member-left-email.handler.ts | 5 +- ...d-owner-recipient-expired-email.handler.ts | 10 +- .../send-recipient-removed-email.handler.ts | 105 + .../emails/send-recipient-removed-email.ts | 34 + .../send-recipient-signed-email.handler.ts | 5 +- .../emails/send-rejection-emails.handler.ts | 10 +- .../emails/send-signing-email.handler.ts | 10 +- .../admin-delete-organisation.handler.ts | 48 +- .../alert-organisation-seat-drift.handler.ts | 67 + .../internal/alert-organisation-seat-drift.ts | 30 + .../internal/backport-subscription-claims.ts | 4 +- .../internal/bulk-send-template.handler.ts | 5 +- .../process-signing-reminder.handler.ts | 18 +- .../internal/seal-document.handler.ts | 29 + .../sync-organisation-seats.handler.ts | 54 + .../internal/sync-organisation-seats.ts | 29 + packages/lib/package.json | 15 +- .../2fa/email/send-2fa-token-email.ts | 5 +- .../server-only/admin/admin-global-search.ts | 372 + .../admin/admin-super-delete-document.ts | 5 +- .../server-only/admin/get-documents-stats.ts | 3 +- packages/lib/server-only/ai/pdf-to-images.ts | 2 +- .../server-only/auth/send-forgot-password.ts | 7 +- .../branding/load-recipient-branding.ts | 6 +- .../branding/store-branding-logo.ts | 26 + .../document-meta/upsert-document-meta.ts | 21 + .../server-only/document/cancel-document.ts | 129 + .../document/complete-document-with-token.ts | 87 +- .../server-only/document/delete-document.ts | 127 +- .../server-only/document/find-documents.ts | 61 +- .../lib/server-only/document/get-stats.ts | 37 +- .../document/reject-document-on-behalf-of.ts | 215 + .../document/reject-document-with-token.ts | 8 + .../server-only/document/resend-document.ts | 67 +- .../document/send-completed-email.ts | 251 - .../server-only/document/send-delete-email.ts | 5 +- .../lib/server-only/document/send-document.ts | 33 +- .../email/email-transport-config.ts | 107 + .../server-only/email/get-email-context.ts | 105 +- .../email/resolve-email-transport.ts | 42 + .../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 +- .../replace-envelope-item-pdf.ts | 3 + .../envelope/assert-envelope-mutable.ts | 83 + .../server-only/envelope/create-envelope.ts | 38 +- .../envelope/duplicate-envelope.ts | 12 + .../server-only/envelope/find-envelopes.ts | 12 + ...et-envelope-for-direct-template-signing.ts | 12 + .../get-envelope-for-recipient-signing.ts | 8 +- .../lib/server-only/envelope/query-helpers.ts | 27 + .../server-only/envelope/update-envelope.ts | 21 + .../field/create-envelope-fields.ts | 5 + .../field/delete-document-field.ts | 5 + .../field/update-envelope-fields.ts | 5 + .../lib/server-only/konva/skia-backend.ts | 5 +- .../license/assert-licensed-for.ts | 69 + .../accept-organisation-invitation.ts | 43 +- .../create-organisation-member-invites.ts | 35 +- .../organisation/create-organisation.ts | 35 +- .../organisation/delete-organisation-email.ts | 11 +- .../organisation/delete-organisation.ts | 55 + packages/lib/server-only/pdf/helpers.ts | 2 +- .../server-only/pdf/insert-field-in-pdf-v2.ts | 2 +- .../lib/server-only/pdf/render-audit-logs.ts | 12 +- .../lib/server-only/pdf/render-certificate.ts | 15 +- .../public-api/get-user-by-token.ts | 44 - .../rate-limit/check-monthly-quota.ts | 31 +- .../rate-limit/compute-quota-flags.ts | 34 + .../rate-limit/get-quota-alert-kind.ts | 40 + .../lib/server-only/rate-limit/rate-limits.ts | 12 +- packages/lib/server-only/rate-limit/types.ts | 11 - .../recipient/create-envelope-recipients.ts | 13 + .../recipient/delete-envelope-recipient.ts | 77 +- .../recipient/get-is-recipient-turn.ts | 7 +- .../recipient/get-next-pending-recipient.ts | 7 +- .../recipient/get-recipient-by-id.ts | 22 + .../recipient/set-document-recipients.ts | 114 +- .../recipient/set-template-recipients.ts | 8 + .../recipient/update-envelope-recipients.ts | 17 + .../update-recipient-next-reminder.ts | 23 +- .../share/create-or-get-share-link.ts | 27 + .../assert-compatible-dictate-next-signer.ts | 35 + .../assert-compatible-recipient-role.ts | 33 + .../assert-compatible-signing-order.ts | 41 + .../resolve-signature-level.ts | 87 + .../signature-level/resolve-signing-order.ts | 36 + .../subscription/get-subscription-claim.ts | 8 - .../team/create-team-email-verification.ts | 5 +- .../lib/server-only/team/delete-team-email.ts | 5 +- packages/lib/server-only/team/delete-team.ts | 6 +- .../team/get-team-email-by-email.ts | 37 - .../team/get-team-public-profile.ts | 24 +- packages/lib/server-only/team/get-teams.ts | 4 +- .../team/update-team-public-profile.ts | 7 +- .../create-document-from-direct-template.ts | 28 +- .../template/create-document-from-template.ts | 48 +- packages/lib/server-only/user/delete-user.ts | 41 +- .../lib/server-only/user/forgot-password.ts | 41 +- packages/lib/server-only/user/verify-email.ts | 12 +- .../webhooks/assert-webhook-url.ts | 13 +- .../webhooks/get-webhooks-by-team-id.ts | 34 +- .../server-only/webhooks/is-private-url.ts | 7 +- .../webhooks/trigger/generate-sample-data.ts | 9 + .../server-only/webhooks/zapier/subscribe.ts | 40 +- .../webhooks/zapier/unsubscribe.ts | 30 +- packages/lib/translations/de/web.po | 1946 +- packages/lib/translations/en/web.po | 1937 +- packages/lib/translations/es/web.po | 1948 +- packages/lib/translations/fr/web.po | 1950 +- packages/lib/translations/it/web.po | 1950 +- packages/lib/translations/ja/web.po | 1952 +- packages/lib/translations/ko/web.po | 1946 +- packages/lib/translations/nl/web.po | 1950 +- packages/lib/translations/pl/web.po | 2202 +- packages/lib/translations/pt-BR/web.po | 1943 +- packages/lib/translations/zh/web.po | 1948 +- packages/lib/types/csc-session.ts | 28 + packages/lib/types/document-audit-logs.ts | 103 +- packages/lib/types/license.ts | 9 + packages/lib/types/name.test.ts | 145 + packages/lib/types/name.ts | 68 + packages/lib/types/organisation.ts | 1 + packages/lib/types/signature-level.ts | 33 + packages/lib/types/subscription.ts | 184 +- packages/lib/types/webhook-payload.ts | 4 + .../field-renderer/field-canvas-style.test.ts | 114 + .../field-renderer/field-canvas-style.ts | 168 + .../field-renderer/field-generic-items.ts | 14 +- .../field-renderer/field-renderer.ts | 23 +- .../universal/field-renderer/render-field.ts | 34 +- .../field-renderer/render-signature-field.ts | 10 +- packages/lib/universal/get-base-url.ts | 4 +- packages/lib/universal/id.ts | 1 + packages/lib/universal/quota-usage.test.ts | 99 + packages/lib/universal/quota-usage.ts | 57 + packages/lib/universal/upload/get-file.ts | 4 +- .../universal/upload/providers/s3-provider.ts | 8 + packages/lib/universal/upload/put-file.ts | 97 +- packages/lib/utils/billing.ts | 47 +- packages/lib/utils/document-audit-logs.ts | 63 +- packages/lib/utils/document.ts | 16 +- packages/lib/utils/email-branding-colors.ts | 105 + packages/lib/utils/env.ts | 33 + packages/lib/utils/envelope.ts | 5 +- packages/lib/utils/fields-overlap.ts | 123 + packages/lib/utils/images/logo.ts | 12 + packages/lib/utils/is-http-url.test.ts | 45 + packages/lib/utils/is-http-url.ts | 32 + packages/lib/utils/is-valid-return-to.ts | 30 +- packages/lib/utils/organisations-claims.ts | 1 + packages/lib/utils/recipients.test.ts | 71 + packages/lib/utils/recipients.ts | 56 +- packages/lib/utils/settings-nav.ts | 299 + packages/lib/utils/settings-switcher.ts | 57 + .../utils/team-global-settings-to-branding.ts | 10 + packages/lib/utils/teams.ts | 10 +- packages/lib/vitest.config.ts | 3 + packages/prettier-config/index.cjs | 42 - packages/prettier-config/package.json | 15 - .../migration.sql | 54 + .../migration.sql | 28 + .../migration.sql | 1 + .../migration.sql | 2 + packages/prisma/package.json | 2 +- packages/prisma/schema.prisma | 107 +- packages/prisma/seed/documents.ts | 97 +- packages/prisma/seed/initial-seed.ts | 11 +- packages/prisma/seed/templates.ts | 42 +- .../prisma/types/extended-document-status.ts | 1 + packages/tailwind-config/package.json | 2 +- packages/trpc/package.json | 6 +- .../trpc/server/admin-router/admin-search.ts | 15 + .../server/admin-router/admin-search.types.ts | 37 + .../create-admin-organisation.types.ts | 5 +- .../admin-router/create-subscription-claim.ts | 2 + .../create-subscription-claim.types.ts | 5 +- .../server/admin-router/create-user.types.ts | 2 +- .../delete-organisation-member.ts | 31 +- .../email-transport/create-email-transport.ts | 32 + .../create-email-transport.types.ts | 16 + .../email-transport/delete-email-transport.ts | 24 + .../delete-email-transport.types.ts | 7 + .../email-transport/find-email-transports.ts | 65 + .../find-email-transports.types.ts | 31 + .../send-test-email-transport.ts | 49 + .../send-test-email-transport.types.ts | 10 + .../email-transport/update-email-transport.ts | 57 + .../update-email-transport.types.ts | 34 + .../find-subscription-claims.types.ts | 1 + .../server/admin-router/get-admin-team.ts | 1 + .../admin-router/get-admin-team.types.ts | 3 + packages/trpc/server/admin-router/router.ts | 14 + .../sync-organisation-subscription.ts | 47 +- .../update-admin-organisation.types.ts | 5 +- .../admin-router/update-subscription-claim.ts | 9 +- .../update-subscription-claim.types.ts | 3 + .../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 +- .../attachment/create-attachment.ts | 4 +- .../attachment/delete-attachment.ts | 4 +- .../attachment/find-attachments.ts | 4 +- .../attachment/update-attachment.ts | 4 +- .../create-document-temporary.types.ts | 2 +- .../document-router/create-document.types.ts | 4 +- .../document-router/delete-document.types.ts | 3 + .../distribute-document.types.ts | 4 +- .../download-document-audit-logs.ts | 12 +- .../download-document-audit-logs.types.ts | 2 +- .../document-router/download-document-beta.ts | 9 +- .../download-document-beta.types.ts | 4 +- .../download-document-certificate.ts | 18 +- .../download-document-certificate.types.ts | 2 +- .../download-document.types.ts | 3 + .../duplicate-document.types.ts | 3 + .../find-documents-internal.ts | 2 + .../find-documents-internal.types.ts | 2 + .../server/document-router/find-documents.ts | 14 +- .../document-router/find-documents.types.ts | 9 +- .../trpc/server/document-router/find-inbox.ts | 16 +- .../document-router/get-document.types.ts | 4 +- .../get-documents-by-ids.types.ts | 4 +- .../server/document-router/get-inbox-count.ts | 2 +- .../redistribute-document.types.ts | 3 +- .../document-router/update-document.types.ts | 3 + .../create-organisation-email.types.ts | 3 +- .../enterprise-router/create-subscription.ts | 6 +- .../create-subscription.types.ts | 1 - .../enterprise-router/csc-sign-envelope.ts | 43 + .../csc-sign-envelope.types.ts | 13 + .../enterprise-router/get-subscription.ts | 63 + .../enterprise-router/manage-subscription.ts | 6 +- .../manage-subscription.types.ts | 1 - .../trpc/server/enterprise-router/router.ts | 6 + .../enterprise-router/sync-subscription.ts | 70 + .../sync-subscription.types.ts | 10 + .../attachment/create-attachment.types.ts | 3 +- .../attachment/update-attachment.types.ts | 3 +- .../envelope-router/bulk-cancel-envelopes.ts | 95 + .../bulk-cancel-envelopes.types.ts | 21 + .../server/envelope-router/cancel-envelope.ts | 37 + .../envelope-router/cancel-envelope.types.ts | 24 + .../envelope-router/delete-envelope.types.ts | 3 +- .../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 + .../envelope-router/duplicate-envelope.ts | 6 +- .../duplicate-envelope.types.ts | 12 +- .../get-envelope-recipient.ts | 22 + .../reject-envelope-recipient-on-behalf-of.ts | 65 + ...t-envelope-recipient-on-behalf-of.types.ts | 35 + .../server/envelope-router/find-envelopes.ts | 16 +- .../envelope-router/find-envelopes.types.ts | 5 + .../redistribute-envelope.types.ts | 2 +- .../replace-envelope-item-pdf.ts | 3 + .../trpc/server/envelope-router/router.ts | 12 + .../envelope-router/sign-envelope-field.ts | 7 + .../envelope-router/update-envelope.types.ts | 20 +- packages/trpc/server/field-router/router.ts | 124 +- packages/trpc/server/folder-router/schema.ts | 5 +- .../create-organisation-group.types.ts | 3 +- .../create-organisation.ts | 66 +- .../create-organisation.types.ts | 8 +- .../delete-organisation-group.ts | 19 +- .../delete-organisation-member-invites.ts | 31 - .../delete-organisation-members.ts | 92 +- .../delete-organisation.ts | 40 +- .../get-organisation-quota-flags.ts | 55 + .../get-organisation-quota-flags.types.ts | 20 + .../organisation-router/leave-organisation.ts | 39 +- .../resend-organisation-member-invite.ts | 18 +- .../trpc/server/organisation-router/router.ts | 4 + .../update-organisation-branding-logo.ts | 69 + ...update-organisation-branding-logo.types.ts | 17 + .../update-organisation-group.ts | 18 + .../update-organisation-group.types.ts | 3 +- .../update-organisation-settings.ts | 13 +- .../update-organisation-settings.types.ts | 1 - packages/trpc/server/profile-router/router.ts | 6 + packages/trpc/server/profile-router/schema.ts | 2 +- .../trpc/server/recipient-router/router.ts | 148 +- .../trpc/server/recipient-router/schema.ts | 18 + .../complete-team-email-verification.ts | 87 + .../complete-team-email-verification.types.ts | 11 + .../server/team-router/create-team.types.ts | 5 +- .../server/team-router/delete-team-group.ts | 9 + packages/trpc/server/team-router/router.ts | 32 +- packages/trpc/server/team-router/schema.ts | 11 +- .../team-router/update-team-branding-logo.ts | 69 + .../update-team-branding-logo.types.ts | 17 + .../server/team-router/update-team-group.ts | 7 +- .../team-router/update-team-settings.ts | 13 +- .../team-router/update-team-settings.types.ts | 1 - .../server/team-router/update-team.types.ts | 5 +- .../get-templates-by-ids.types.ts | 4 +- .../trpc/server/template-router/router.ts | 39 +- packages/trpc/server/webhook-router/schema.ts | 8 + packages/trpc/utils/zod-form-data.ts | 20 + packages/tsconfig/process-env.d.ts | 18 +- .../common/language-switcher-dialog.tsx | 3 +- .../document/document-email-checkboxes.tsx | 675 +- .../envelope-recipient-field-tooltip.tsx | 141 +- packages/ui/components/field/field.tsx | 3 +- packages/ui/components/signing-card.tsx | 2 +- .../ui/lib/field-root-container-classes.ts | 14 + packages/ui/package.json | 44 +- packages/ui/primitives/alert-dialog.tsx | 2 +- packages/ui/primitives/avatar.tsx | 2 +- packages/ui/primitives/command.tsx | 6 +- packages/ui/primitives/dialog.tsx | 2 +- .../primitives/document-flow/add-fields.tsx | 2 +- .../primitives/document-flow/add-signers.tsx | 153 +- .../primitives/document-flow/add-subject.tsx | 2 + .../document-flow/field-content.tsx | 9 +- .../dropdown-field.tsx | 2 +- .../ui/primitives/document-upload-button.tsx | 8 +- packages/ui/primitives/radio-group.tsx | 41 +- .../ui/primitives/recipient-role-icons.tsx | 1 + packages/ui/primitives/sheet.tsx | 163 +- .../signature-pad/signature-pad-draw.tsx | 2 +- .../signature-pad/signature-render.tsx | 30 +- .../template-flow/add-template-fields.tsx | 6 +- packages/ui/styles/theme.css | 34 +- patches/@ai-sdk+google-vertex+3.0.81.patch | 884 - prettier.config.cjs | 1 - render.yaml | 14 +- scripts/create-justification.ts | 73 - scripts/create-plan.ts | 73 - scripts/create-scratch.ts | 73 - scripts/utils/generate-id.ts | 84 - tsconfig.eslint.json | 4 - turbo.json | 13 +- 908 files changed, 68508 insertions(+), 29032 deletions(-) create mode 100644 .agents/plans/sharp-gold-mountain-custom-brand-logo-url.md create mode 100644 .agents/plans/wild-indigo-wave-rejected-expired-recipient-filters.md delete mode 100644 .agents/skills/create-justification/SKILL.md delete mode 100644 .agents/skills/create-plan/SKILL.md delete mode 100644 .agents/skills/create-scratch/SKILL.md delete mode 100644 .eslintignore delete mode 100644 .eslintrc.cjs 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 delete mode 100644 .prettierignore create mode 100644 SECURITY.md create mode 100644 apps/docs/content/docs/developers/api/migrate-to-envelopes.mdx delete mode 100644 apps/docs/content/docs/developers/embedding/authoring/index.mdx delete mode 100644 apps/docs/content/docs/developers/embedding/authoring/meta.json delete mode 100644 apps/docs/content/docs/developers/embedding/authoring/v1.mdx delete mode 100644 apps/docs/content/docs/developers/embedding/authoring/v2.mdx create mode 100644 apps/docs/content/docs/policies/verify-email.mdx create mode 100644 apps/docs/content/docs/self-hosting/configuration/license.mdx create mode 100644 apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx create mode 100644 apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx create mode 100644 apps/docs/public/get-started-images/documenso-registration-form.webp create mode 100644 apps/docs/src/components/mdx/envelope-warning.tsx delete mode 100644 apps/remix/Dockerfile create mode 100644 apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx delete mode 100644 apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx delete mode 100644 apps/remix/app/components/dialogs/document-resend-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/email-transport-create-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/email-transport-update-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/envelopes-bulk-cancel-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/envelopes-bulk-download-dialog.tsx delete mode 100644 apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx create mode 100644 apps/remix/app/components/dialogs/token-create-dialog.tsx create mode 100644 apps/remix/app/components/forms/certificate-preferences-form.tsx create mode 100644 apps/remix/app/components/forms/email-transport-form.tsx create mode 100644 apps/remix/app/components/forms/form-sticky-save-bar.tsx create mode 100644 apps/remix/app/components/forms/inheritable-field.tsx create mode 100644 apps/remix/app/components/forms/reminder-preferences-form.tsx delete mode 100644 apps/remix/app/components/forms/token.tsx create mode 100644 apps/remix/app/components/general/app-command-menu.types.ts create mode 100644 apps/remix/app/components/general/direct-template/direct-template-invalid-page.tsx create mode 100644 apps/remix/app/components/general/document-signing/csc-recipient-blocked-page.tsx create mode 100644 apps/remix/app/components/general/document-signing/csc-recipient-signing-in-progress-page.tsx create mode 100644 apps/remix/app/components/general/envelope-editor/envelope-editor-invalid-direct-template-alert.tsx create mode 100644 apps/remix/app/components/general/filter-pill.tsx delete mode 100644 apps/remix/app/components/general/menu-switcher.tsx create mode 100644 apps/remix/app/components/general/organisations/organisation-quota-banner.tsx delete mode 100644 apps/remix/app/components/general/settings-nav-desktop.tsx delete mode 100644 apps/remix/app/components/general/settings-nav-mobile.tsx create mode 100644 apps/remix/app/components/general/settings-org-switcher.tsx create mode 100644 apps/remix/app/components/general/settings-scope-breadcrumb.tsx create mode 100644 apps/remix/app/components/general/settings-team-switcher.tsx create mode 100644 apps/remix/app/components/general/settings-upsell/branding-upsell.tsx create mode 100644 apps/remix/app/components/general/settings-upsell/email-domains-upsell.tsx create mode 100644 apps/remix/app/components/general/settings-upsell/motion.ts create mode 100644 apps/remix/app/components/general/settings-upsell/settings-upsell-card.tsx create mode 100644 apps/remix/app/components/general/settings-upsell/sso-portal-upsell.tsx create mode 100644 apps/remix/app/components/general/settings-upsell/use-timed-cycle.ts delete mode 100644 apps/remix/app/components/general/teams/team-email-dropdown.tsx create mode 100644 apps/remix/app/components/general/unified-settings-layout.tsx create mode 100644 apps/remix/app/components/general/unified-settings-sidebar-mobile.tsx create mode 100644 apps/remix/app/components/general/unified-settings-sidebar.tsx create mode 100644 apps/remix/app/components/general/use-admin-search-categories.ts create mode 100644 apps/remix/app/components/tables/admin-email-transports-table.tsx create mode 100644 apps/remix/app/components/tables/documents-table-period-filter.tsx create mode 100644 apps/remix/app/components/tables/documents-table-status-filter.tsx create mode 100644 apps/remix/app/routes/_authenticated+/admin+/email-transports._index.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/o.$orgUrl.settings._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.certificates.tsx create mode 100644 apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.reminders.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/_layout.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/billing-personal.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/branding.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/document.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/email.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/public-profile.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/tokens.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/webhooks.$id._index.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_dynamic_personal_routes+/webhooks._index.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/settings+/_index.tsx delete mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings._index.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.certificates.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.general.tsx create mode 100644 apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.reminders.tsx create mode 100644 apps/remix/app/routes/api+/preferred-team.tsx create mode 100644 apps/remix/app/utils/documents-search-params.ts create mode 100644 apps/remix/app/utils/toast-error-messages.ts create mode 100644 packages/app-tests/e2e/admin/email-transports/email-transport-claims.spec.ts create mode 100644 packages/app-tests/e2e/admin/email-transports/email-transport-crud.spec.ts create mode 100644 packages/app-tests/e2e/admin/global-search.spec.ts create mode 100644 packages/app-tests/e2e/api/trpc/admin/admin-search.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/redistribute-send-status.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/reject-recipient-on-behalf-of.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cancel.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-envelope-cert-audit-log.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-download.spec.ts create mode 100644 packages/app-tests/e2e/api/v2/unauthorized-api-access/api-access-file-upload.spec.ts 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/app-tests/e2e/documents/cancel-documents.spec.ts 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/envelope-editor-v2/envelope-direct-template-validation.spec.ts create mode 100644 packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-autosave-race.spec.ts create mode 100644 packages/app-tests/e2e/envelope-editor-v2/envelope-recipient-cc-order.spec.ts create mode 100644 packages/app-tests/e2e/fixtures/command-menu.ts create mode 100644 packages/app-tests/e2e/organisations/organisation-permission-hierarchy.spec.ts create mode 100644 packages/app-tests/e2e/organisations/organisation-quota-banner.spec.ts create mode 100644 packages/app-tests/e2e/recipient/recipient-visibility-access.spec.ts create mode 100644 packages/app-tests/e2e/settings/preferred-team-cookie.spec.ts create mode 100644 packages/app-tests/e2e/settings/unified-settings.spec.ts create mode 100644 packages/app-tests/e2e/signing-branding.spec.ts create mode 100644 packages/app-tests/e2e/teams/team-groups.spec.ts create mode 100644 packages/app-tests/e2e/teams/team-members.spec.ts create mode 100644 packages/app-tests/e2e/teams/team-profile-access.spec.ts create mode 100644 packages/app-tests/e2e/teams/team-settings-save-bar.spec.ts create mode 100644 packages/app-tests/e2e/templates/template-use-dialog.spec.ts create mode 100644 packages/app-tests/e2e/webhooks/webhook-secret-access.spec.ts create mode 100644 packages/ee/server-only/signing/csc/algorithm-resolver.ts create mode 100644 packages/ee/server-only/signing/csc/cert-chain.ts create mode 100644 packages/ee/server-only/signing/csc/ciphers.ts create mode 100644 packages/ee/server-only/signing/csc/client/credentials.ts create mode 100644 packages/ee/server-only/signing/csc/client/http.ts create mode 100644 packages/ee/server-only/signing/csc/client/index.ts create mode 100644 packages/ee/server-only/signing/csc/client/info.ts create mode 100644 packages/ee/server-only/signing/csc/client/oauth.ts create mode 100644 packages/ee/server-only/signing/csc/client/signatures.ts create mode 100644 packages/ee/server-only/signing/csc/client/types.ts create mode 100644 packages/ee/server-only/signing/csc/cookies/blocking-error-cookie.ts create mode 100644 packages/ee/server-only/signing/csc/cookies/oauth-flow-cookie.ts create mode 100644 packages/ee/server-only/signing/csc/cookies/sad-session-cookie.ts create mode 100644 packages/ee/server-only/signing/csc/cookies/service-session-cookie.ts create mode 100644 packages/ee/server-only/signing/csc/cookies/shared.ts create mode 100644 packages/ee/server-only/signing/csc/credential.ts create mode 100644 packages/ee/server-only/signing/csc/execute-tsp-sign.ts create mode 100644 packages/ee/server-only/signing/csc/finalize-tsp-completion.ts create mode 100644 packages/ee/server-only/signing/csc/hono/context.ts create mode 100644 packages/ee/server-only/signing/csc/hono/index.ts create mode 100644 packages/ee/server-only/signing/csc/hono/oauth-authorize.ts create mode 100644 packages/ee/server-only/signing/csc/hono/oauth-callback.ts create mode 100644 packages/ee/server-only/signing/csc/materialize-anchors.ts create mode 100644 packages/ee/server-only/signing/csc/pdf-names.ts create mode 100644 packages/ee/server-only/signing/csc/prepare-recipient-signing.ts create mode 100644 packages/ee/server-only/signing/csc/render-overlay.ts create mode 100644 packages/ee/server-only/signing/csc/sign-session.ts create mode 100644 packages/ee/server-only/signing/csc/signers/capture-signer.ts create mode 100644 packages/ee/server-only/signing/csc/signers/fifo-signer.ts create mode 100644 packages/ee/server-only/signing/csc/transport.ts create mode 100644 packages/ee/server-only/signing/csc/tsa-resolver.ts create mode 100644 packages/ee/server-only/signing/csc/tsp-timestamp-authority.ts create mode 100644 packages/ee/server-only/stripe/sync-stripe-customer-subscription.ts delete mode 100644 packages/ee/server-only/stripe/webhook/on-subscription-created.ts delete mode 100644 packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts delete mode 100644 packages/ee/server-only/stripe/webhook/on-subscription-updated.ts create mode 100644 packages/email/preview/.gitignore create mode 100644 packages/email/preview/app/app.css create mode 100644 packages/email/preview/app/components/playground.tsx create mode 100644 packages/email/preview/app/components/prop-fields.tsx create mode 100644 packages/email/preview/app/entry.client.tsx create mode 100644 packages/email/preview/app/entry.server.tsx create mode 100644 packages/email/preview/app/lib/templates.tsx create mode 100644 packages/email/preview/app/lib/viewports.ts create mode 100644 packages/email/preview/app/root.tsx create mode 100644 packages/email/preview/app/routes.ts create mode 100644 packages/email/preview/app/routes/$slug.tsx create mode 100644 packages/email/preview/app/routes/_index.tsx create mode 100644 packages/email/preview/app/routes/api.render.tsx create mode 100644 packages/email/preview/postcss.config.cjs create mode 100644 packages/email/preview/react-router.config.ts create mode 100644 packages/email/preview/tailwind.config.cjs create mode 100644 packages/email/preview/tsconfig.json create mode 100644 packages/email/preview/vite.config.ts create mode 100644 packages/email/template-components/template-branding-logo.tsx create mode 100644 packages/email/templates/organisation-limit-alert.tsx delete mode 100644 packages/email/templates/organisation-limit-exceeded.tsx create mode 100644 packages/email/transports/build-transport.ts create mode 100644 packages/email/transports/normalize-headers.ts create mode 100644 packages/email/utils/branding-url.ts delete mode 100644 packages/eslint-config/index.cjs delete mode 100644 packages/eslint-config/package.json delete mode 100644 packages/eslint-config/tsconfig.json create mode 100644 packages/lib/client-only/cookies.ts create mode 100644 packages/lib/client-only/create-zip-writer.ts create mode 100644 packages/lib/client-only/hooks/use-child-route-flags.ts create mode 100644 packages/lib/constants/cookies.ts create mode 100644 packages/lib/constants/pdf.test.ts 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} (58%) create mode 100644 packages/lib/jobs/definitions/emails/send-document-pending-email.ts create mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.handler.ts create mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limit-alert-email.ts delete mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limit-exceeded-email.handler.ts delete mode 100644 packages/lib/jobs/definitions/emails/send-organisation-limit-exceeded-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 create mode 100644 packages/lib/jobs/definitions/internal/alert-organisation-seat-drift.handler.ts create mode 100644 packages/lib/jobs/definitions/internal/alert-organisation-seat-drift.ts create mode 100644 packages/lib/jobs/definitions/internal/sync-organisation-seats.handler.ts create mode 100644 packages/lib/jobs/definitions/internal/sync-organisation-seats.ts create mode 100644 packages/lib/server-only/admin/admin-global-search.ts create mode 100644 packages/lib/server-only/branding/store-branding-logo.ts create mode 100644 packages/lib/server-only/document/cancel-document.ts create mode 100644 packages/lib/server-only/document/reject-document-on-behalf-of.ts delete mode 100644 packages/lib/server-only/document/send-completed-email.ts create mode 100644 packages/lib/server-only/email/email-transport-config.ts create mode 100644 packages/lib/server-only/email/resolve-email-transport.ts create mode 100644 packages/lib/server-only/envelope/assert-envelope-mutable.ts create mode 100644 packages/lib/server-only/envelope/query-helpers.ts create mode 100644 packages/lib/server-only/license/assert-licensed-for.ts create mode 100644 packages/lib/server-only/organisation/delete-organisation.ts delete mode 100644 packages/lib/server-only/public-api/get-user-by-token.ts create mode 100644 packages/lib/server-only/rate-limit/compute-quota-flags.ts create mode 100644 packages/lib/server-only/rate-limit/get-quota-alert-kind.ts create mode 100644 packages/lib/server-only/signature-level/assert-compatible-dictate-next-signer.ts create mode 100644 packages/lib/server-only/signature-level/assert-compatible-recipient-role.ts create mode 100644 packages/lib/server-only/signature-level/assert-compatible-signing-order.ts create mode 100644 packages/lib/server-only/signature-level/resolve-signature-level.ts create mode 100644 packages/lib/server-only/signature-level/resolve-signing-order.ts delete mode 100644 packages/lib/server-only/team/get-team-email-by-email.ts create mode 100644 packages/lib/types/csc-session.ts create mode 100644 packages/lib/types/name.test.ts create mode 100644 packages/lib/types/name.ts create mode 100644 packages/lib/types/signature-level.ts create mode 100644 packages/lib/universal/field-renderer/field-canvas-style.test.ts create mode 100644 packages/lib/universal/field-renderer/field-canvas-style.ts create mode 100644 packages/lib/universal/quota-usage.test.ts create mode 100644 packages/lib/universal/quota-usage.ts create mode 100644 packages/lib/utils/email-branding-colors.ts create mode 100644 packages/lib/utils/fields-overlap.ts create mode 100644 packages/lib/utils/is-http-url.test.ts create mode 100644 packages/lib/utils/is-http-url.ts create mode 100644 packages/lib/utils/recipients.test.ts create mode 100644 packages/lib/utils/settings-nav.ts create mode 100644 packages/lib/utils/settings-switcher.ts delete mode 100644 packages/prettier-config/index.cjs delete mode 100644 packages/prettier-config/package.json create mode 100644 packages/prisma/migrations/20260525103410_add_signature_level_and_csc_tables/migration.sql create mode 100644 packages/prisma/migrations/20260604143030_add_email_transports/migration.sql create mode 100644 packages/prisma/migrations/20260616120000_add_cancelled_document_status/migration.sql create mode 100644 packages/prisma/migrations/20260622120000_add_recipient_reminder_count/migration.sql create mode 100644 packages/trpc/server/admin-router/admin-search.ts create mode 100644 packages/trpc/server/admin-router/admin-search.types.ts create mode 100644 packages/trpc/server/admin-router/email-transport/create-email-transport.ts create mode 100644 packages/trpc/server/admin-router/email-transport/create-email-transport.types.ts create mode 100644 packages/trpc/server/admin-router/email-transport/delete-email-transport.ts create mode 100644 packages/trpc/server/admin-router/email-transport/delete-email-transport.types.ts create mode 100644 packages/trpc/server/admin-router/email-transport/find-email-transports.ts create mode 100644 packages/trpc/server/admin-router/email-transport/find-email-transports.types.ts create mode 100644 packages/trpc/server/admin-router/email-transport/send-test-email-transport.ts create mode 100644 packages/trpc/server/admin-router/email-transport/send-test-email-transport.types.ts create mode 100644 packages/trpc/server/admin-router/email-transport/update-email-transport.ts create mode 100644 packages/trpc/server/admin-router/email-transport/update-email-transport.types.ts create mode 100644 packages/trpc/server/enterprise-router/csc-sign-envelope.ts create mode 100644 packages/trpc/server/enterprise-router/csc-sign-envelope.types.ts create mode 100644 packages/trpc/server/enterprise-router/sync-subscription.ts create mode 100644 packages/trpc/server/enterprise-router/sync-subscription.types.ts create mode 100644 packages/trpc/server/envelope-router/bulk-cancel-envelopes.ts create mode 100644 packages/trpc/server/envelope-router/bulk-cancel-envelopes.types.ts create mode 100644 packages/trpc/server/envelope-router/cancel-envelope.ts create mode 100644 packages/trpc/server/envelope-router/cancel-envelope.types.ts 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 create mode 100644 packages/trpc/server/envelope-router/envelope-recipients/reject-envelope-recipient-on-behalf-of.ts create mode 100644 packages/trpc/server/envelope-router/envelope-recipients/reject-envelope-recipient-on-behalf-of.types.ts create mode 100644 packages/trpc/server/organisation-router/get-organisation-quota-flags.ts create mode 100644 packages/trpc/server/organisation-router/get-organisation-quota-flags.types.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/complete-team-email-verification.ts create mode 100644 packages/trpc/server/team-router/complete-team-email-verification.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 create mode 100644 packages/ui/lib/field-root-container-classes.ts delete mode 100644 patches/@ai-sdk+google-vertex+3.0.81.patch delete mode 100644 prettier.config.cjs delete mode 100644 scripts/create-justification.ts delete mode 100644 scripts/create-plan.ts delete mode 100644 scripts/create-scratch.ts delete mode 100644 scripts/utils/generate-id.ts delete mode 100644 tsconfig.eslint.json diff --git a/.agents/plans/sharp-gold-mountain-custom-brand-logo-url.md b/.agents/plans/sharp-gold-mountain-custom-brand-logo-url.md new file mode 100644 index 000000000..e1b573c3e --- /dev/null +++ b/.agents/plans/sharp-gold-mountain-custom-brand-logo-url.md @@ -0,0 +1,122 @@ +--- +date: 2026-05-28 +title: Custom Brand Logo Url +--- + +# Problem + +`brandingUrl` (the configured "Brand Website") is persisted and editable in branding +settings, but historically it was never consumed anywhere. It flowed into the database, +the settings form, and the admin read-only view, but never affected any rendered output. + +We want `brandingUrl` to actually do something, with deliberately different behavior per +surface. + +# Relationship we're going for + +`brandingUrl` is an **email-only** linking concept. It is intentionally **not** used on +in-app signing surfaces. + +| Surface | Custom branding logo configured | `brandingUrl` behavior | +| --- | --- | --- | +| Transactional emails (logo) | Logo shown | Logo links to `brandingUrl` when it is a safe http(s) URL; otherwise plain image | +| Transactional emails (footer) | n/a | `brandingUrl` rendered as a link in the footer when it is a safe http(s) URL | +| Signing pages (V1 + V2, normal + direct-template) | Logo shown | Ignored — logo is a plain image with no link | +| Signing pages (no custom logo) | Documenso fallback shown | Fallback keeps its internal `/` link | +| Embedded signing | Logo shown | Ignored (logo not linked) | +| Embedded authoring/editor | Logo shown | Ignored | +| Settings / admin branding previews | n/a | Unchanged (display only) | + +Rationale: + +- On signing pages the recipient is mid-task; sending them off to an external marketing + site via the logo is undesirable, so the custom logo is a plain image there. +- In emails the logo and a footer link to the brand's own site are a normal, expected + pattern and reinforce that the email is legitimately from that brand. + +# Decisions + +## Scope + +- Use `brandingUrl` only in transactional email rendering: + - The shared email logo component links the custom branding logo to `brandingUrl`. + - The shared email footer renders `brandingUrl` as a link. +- On signing surfaces, render a configured custom branding logo as a plain image with no + link wrapper. Leave the Documenso fallback logo's internal `/` link untouched. +- Do not change embedded signing, embedded authoring/editor, or settings/admin previews. +- No Prisma schema or database migration. `brandingUrl` already exists and is editable. + +## URL safety + +Rendering must be defensive because old/imported data can bypass the branding form's URL +validation. Only treat the stored value as a usable Brand Website when it parses as an +absolute `http:` or `https:` URL. + +- Empty, missing, invalid, relative, or non-http(s) values are treated as "no Brand + Website" and produce a plain logo / no footer link. +- Do not mutate stored settings or run a cleanup migration. +- Factored into a single shared helper so both email logo and footer apply identical rules: + - `packages/email/utils/branding-url.ts` -> `getSafeBrandingUrl(value): string | null`. + +## Email rendering + +- New shared component `packages/email/template-components/template-branding-logo.tsx` + (`TemplateBrandingLogo`) renders either: + - the custom branding logo, wrapped in a `Link` to the safe `brandingUrl` with + `target="_blank"` when one exists, or a plain `Img` when not; or + - the Documenso fallback logo (`/static/logo.png`) when custom branding is disabled or + no logo is set. +- This component replaced the duplicated `brandingEnabled && brandingLogo ? : ` + ternary that was copy-pasted across all transactional email templates. +- `packages/email/template-components/template-footer.tsx` renders `brandingUrl` as a + footer link (via `getSafeBrandingUrl`) when branding is enabled and the URL is safe. + +The branding context already exposes `brandingUrl` (`packages/email/providers/branding.tsx`), +populated by `teamGlobalSettingsToBranding` / `organisationGlobalSettingsToBranding` +(which spread `...settings`), so no additional plumbing into the email branding context was +required. + +## Signing rendering + +- `apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx`: + custom logo renders as a bare ``. `brandingUrl` is not read; the local branding type + and loader payload no longer carry it. +- `apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx` (V2, + shared by normal and direct-template signing): custom logo renders as a bare ``; the + Documenso fallback keeps its ``. +- `apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx`: V1 loader branding payload no + longer includes `brandingUrl`. +- `packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts` and + `get-envelope-for-direct-template-signing.ts`: `brandingUrl` removed from the V2 + `EnvelopeForSigningResponse.settings` schema/payload since it is not consumed there. + +# History + +An earlier iteration of this plan wired `brandingUrl` into the in-app signing pages so a +custom logo linked to the Brand Website (external ``, internal `/` +fallback otherwise) and added `brandingUrl` to the V1/V2 signing payloads. That direction +was reversed: signing-page logos are now plain images and `brandingUrl` is email-only. The +signing payload additions were removed. + +# Test coverage + +`packages/app-tests/e2e/signing-branding.spec.ts`: + +- V1 normal `/sign/:token`: custom logo is a plain image, not inside a link, and no + `brandingUrl` link is present. +- V2 normal `/sign/:token` and V2 direct-template: same plain-image assertions. +- V2 with no custom logo: Documenso fallback still links to `/`. +- Embedded signing: no custom-logo Brand Website link is rendered. + +# Acceptance criteria + +- A custom branding logo on any signing surface (V1, V2 normal, V2 direct-template, embedded) + renders as a plain image with no link, and `brandingUrl` is never rendered as a link there. +- Documenso fallback logos continue linking to `/`. +- In transactional emails, when a custom logo and a safe `brandingUrl` are configured, the + email logo links to `brandingUrl` (new tab) and the footer shows the Brand Website link. +- In transactional emails, when `brandingUrl` is empty/invalid/relative/non-http(s), the logo + is a plain image and no footer Brand Website link is shown. +- URL safety is enforced through the single shared `getSafeBrandingUrl` helper. +- Settings/admin branding previews are unchanged. +- No schema or migration changes. diff --git a/.agents/plans/wild-indigo-wave-rejected-expired-recipient-filters.md b/.agents/plans/wild-indigo-wave-rejected-expired-recipient-filters.md new file mode 100644 index 000000000..b8e9fb39c --- /dev/null +++ b/.agents/plans/wild-indigo-wave-rejected-expired-recipient-filters.md @@ -0,0 +1,146 @@ +--- +date: 2026-05-28 +title: Rejected Expired Recipient Filters +--- + +## Context + +Customers need to find (a) envelopes/documents in the `REJECTED` state and (b) envelopes +with at least one recipient whose signing link has **expired**. Today the UI only exposes +`INBOX / PENDING / COMPLETED / DRAFT / ALL` tabs, and the public API has no way to filter by +expired recipient links — forcing a fetch-all-`PENDING`-then-inspect-each-recipient workaround. + +Two key facts from exploration shaped this plan: + +- **`REJECTED` is already fully wired in the backend** — the where-clause (`find-documents.ts`), + stats counts (`get-stats.ts`), tRPC response schema, `ExtendedDocumentStatus` enum, and the + `FRIENDLY_STATUS_MAP` display all handle it. It is simply absent from the UI tab array. +- **Renewing expired links already works.** `resendDocument` refreshes `expiresAt` and clears + `expirationNotifiedAt` for unsigned, non-CC recipients (`resend-document.ts:98-121`), exposed + publicly via `POST /api/v2/document/redistribute` and `/api/v2/envelope/redistribute` and via the + resend/redistribute UI dialogs. No new renew mechanism is needed — only documentation/wording. + +Expiration is a per-recipient condition (not an envelope status). The approved design models it +in the UI as an `EXPIRED` **pseudo-status tab** (reusing the existing tab machinery, mirroring how +`REJECTED` works) and in the public API as an orthogonal boolean `hasExpiredRecipients`. Both share +one EXISTS predicate. + +Definition of "expired recipient" (matches `isRecipientExpired`, `packages/lib/utils/recipients.ts:118`): +a `Recipient` with `expiresAt IS NOT NULL AND expiresAt <= now() AND signingStatus = NOT_SIGNED AND role != CC`. + +## Approach + +### A. Shared EXISTS predicate (reused 4x, justified) +Add a local `hasExpiredRecipient(eb)` helper — modeled on the existing per-file `recipientExists` / +`senderEmailIs` helpers — to `find-documents.ts`, `get-stats.ts`, and `find-envelopes.ts`. It is the +single source of truth for the expired condition above (using `new Date()` for `now`, matching the +`period` filter's `.toJSDate()` style). + +### B. REJECTED tab (UI only — backend already done) +- `apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx`: add + `ExtendedDocumentStatus.REJECTED` to the tab array (lines 149-155). Count badge, highlight, and + `?status=REJECTED` filtering already work via existing machinery. + +### C. EXPIRED pseudo-status (UI + internal stats) +1. `packages/prisma/types/extended-document-status.ts`: add `EXPIRED: 'EXPIRED'`. Internal-only — + the public `DocumentStatus` enum is unaffected. This intentionally surfaces TS errors at the three + exhaustive/`Record` sites below, forcing them to be handled. +2. `packages/lib/server-only/document/find-documents.ts`: + - Add `.with(ExtendedDocumentStatus.EXPIRED, ...)` to **both** `applyPersonalFilters` and + `applyTeamFilters`, mirroring the `COMPLETED` branch's access control (deleted + visibility + + owner/recipient access) with `hasExpiredRecipient(eb)` AND-ed in. Do **not** constrain + `Envelope.status` — the EXISTS already restricts to unsigned recipients. +3. `packages/lib/server-only/document/get-stats.ts`: + - Add an `expiredQuery` mirroring `pendingQuery`'s access control + `hasExpiredRecipient(eb)`. + - Add it to the `Promise.all`, add `[ExtendedDocumentStatus.EXPIRED]: expired` to the `stats` + record. **Do not** add `expired` to the `all` sum (it overlaps `PENDING`). +4. `packages/trpc/server/document-router/find-documents-internal.types.ts`: add + `[ExtendedDocumentStatus.EXPIRED]: z.number()` to the `stats` response object. (`status` already + accepts the extended enum via `z.nativeEnum(ExtendedDocumentStatus)`.) +5. `apps/remix/app/components/general/document/document-status.tsx`: add an `EXPIRED` entry to + `FRIENDLY_STATUS_MAP` — `label: msg` Expired, an icon (e.g. lucide `TimerOff`, matching the + `/sign/$token/expired` page), and a distinct color (e.g. `text-orange-500`) to differentiate from + `REJECTED` (red). +6. `documents._index.tsx`: add `[ExtendedDocumentStatus.EXPIRED]: 0` to the `stats` `useState` + initializer and `ExtendedDocumentStatus.EXPIRED` to the tab array. Final order: + `INBOX, PENDING, COMPLETED, DRAFT, REJECTED, EXPIRED, ALL`. +7. (Optional, recommended) `apps/remix/app/components/tables/documents-table-empty-state.tsx`: add + tailored `EXPIRED` and `REJECTED` empty-state copy (currently both fall through to `.otherwise()`). + +### D. Public API boolean `hasExpiredRecipients` (document + envelope, v2) +1. `packages/lib/server-only/document/find-documents.ts`: add `hasExpiredRecipients?: boolean` to + `FindDocumentsOptions`; when true, apply `.where((eb) => hasExpiredRecipient(eb))` inside + `buildBaseQuery` (orthogonal/additive to any `status`). +2. `packages/trpc/server/document-router/find-documents.types.ts`: add a query-safe boolean + `hasExpiredRecipients` to `ZFindDocumentsRequestSchema` with a `.describe(...)`. Mirror the + existing boolean-query-param handling in `find-document-audit-logs.types.ts` + (`filterForRecentActivity`) — avoid raw `z.coerce.boolean()` (the "false" -> true footgun); use a + string transform if needed. Pass it through in `find-documents.ts` (public handler). +3. `packages/lib/server-only/envelope/find-envelopes.ts`: add `hasExpiredRecipients?: boolean` to + `FindEnvelopesOptions` + the `hasExpiredRecipient(eb)` helper + the additive `.where`. +4. `packages/trpc/server/envelope-router/find-envelopes.types.ts`: add the same param to + `ZFindEnvelopesRequestSchema`; pass it through in the envelope-router find handler. + The param auto-appears in the generated `/api/v2/openapi.json`. + +Note: REST v1 `GET /api/v1/documents` is deprecated and lacks status filtering — left unchanged. +`REJECTED` is already a valid public `status` value (`DocumentStatus.REJECTED`), so no API change is +needed for rejected filtering. + +### E. Renew expired links — documentation only +No functional change. Document that resending renews expired links: +- Update the `.description` in `packages/trpc/server/document-router/redistribute-document.types.ts` + and `packages/trpc/server/envelope-router/redistribute-envelope.types.ts` to state that + redistributing refreshes the signing-link expiration for unsigned recipients. +- Optionally adjust resend/redistribute dialog copy + (`apps/remix/app/components/dialogs/document-resend-dialog.tsx`, + `envelope-redistribute-dialog.tsx`) to mention it renews expired links. + +## Files To Modify (summary) + +| Area | File | +|------|------| +| Enum | `packages/prisma/types/extended-document-status.ts` | +| Where-clause + API option | `packages/lib/server-only/document/find-documents.ts` | +| Stats counts | `packages/lib/server-only/document/get-stats.ts` | +| Envelope find (API) | `packages/lib/server-only/envelope/find-envelopes.ts` | +| Internal tRPC stats schema | `packages/trpc/server/document-router/find-documents-internal.types.ts` | +| Public doc API schema + handler | `packages/trpc/server/document-router/find-documents.types.ts`, `find-documents.ts` | +| Public envelope API schema + handler | `packages/trpc/server/envelope-router/find-envelopes.types.ts`, `find-envelopes.ts` | +| Status display | `apps/remix/app/components/general/document/document-status.tsx` | +| Tabs + stats init | `apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents._index.tsx` | +| Empty state (optional) | `apps/remix/app/components/tables/documents-table-empty-state.tsx` | +| Renew docs | `redistribute-document.types.ts`, `redistribute-envelope.types.ts` (+ resend dialogs, optional) | + +## Reused Utilities / Patterns +- `recipientExists` / `senderEmailIs` (per-file Kysely EXISTS helpers) — the template for the new + `hasExpiredRecipient` helper. +- `REJECTED` branches in `find-documents.ts` (lines 279, 416) and `rejectedQuery` in `get-stats.ts` + (line 227) — the template for the `EXPIRED` branches / `expiredQuery`. +- `isRecipientExpired` (`packages/lib/utils/recipients.ts:118`) — defines the `expiresAt <= now` + semantics to match. +- Existing tab machinery in `documents._index.tsx` (`getTabHref`, count badge, personal-org `.filter`) + — works unchanged for the new tabs. +- `resendDocument` / `trpc.document.redistribute` / `trpc.envelope.redistribute` — existing renew path. + +## Verification +1. **Typecheck** (the enum change forces all exhaustive/Record sites): `npm run typecheck -w @documenso/remix`. +2. **Seed + UI** (dev server already running): seed a team via `seedTeam`, send a document, then: + - Reject one as a recipient -> it appears under the new **Rejected** tab with a count. + - Force expiry (set a recipient `expiresAt` in the past, e.g. via Prisma Studio or a short + `envelopeExpirationPeriod`) -> the doc appears under the new **Expired** tab with a count, and the + count excludes signed/CC recipients. +3. **Public API**: `GET /api/v2/document?hasExpiredRecipients=true` and + `GET /api/v2/envelope?hasExpiredRecipients=true` (Bearer API token) return only envelopes with >=1 + expired unsigned recipient; confirm `GET /api/v2/document?status=REJECTED` works. Verify the param + appears in `/api/v2/openapi.json`. +4. **Renew**: on an expired doc, run resend/redistribute (UI dialog or + `POST /api/v2/document/redistribute`) -> recipient `expiresAt` is refreshed, the doc leaves the + Expired tab, and the signing link no longer redirects to `/sign/$token/expired`. +5. **E2E** (optional): extend `packages/app-tests/e2e/envelopes/envelope-expiration-send.spec.ts` + with an Expired-tab assertion. +6. Do **not** modify/commit `packages/lib/translations/*.po`; run `npm run translate` only if needed + for new `msg`/`Trans` strings, and keep generated `.po` files out of the branch. + +## Open Questions +- Exact icon/color for the `EXPIRED` tab (proposed: `TimerOff`, `text-orange-500`). +- Whether to add the optional tailored empty-state copy now or defer. \ No newline at end of file diff --git a/.agents/skills/create-justification/SKILL.md b/.agents/skills/create-justification/SKILL.md deleted file mode 100644 index 78a2aaea9..000000000 --- a/.agents/skills/create-justification/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: create-justification -description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title -license: MIT -compatibility: opencode -metadata: - audience: agents - workflow: decision-making ---- - -## What I do - -I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets: - -- A unique three-word identifier (e.g., `swift-emerald-river`) -- Frontmatter with the current date and formatted title -- Content you provide - -## How to use - -Run the script with a slug and content: - -```bash -npx tsx scripts/create-justification.ts "decision-name" "Justification content here" -``` - -Or use heredoc for multi-line content: - -```bash -npx tsx scripts/create-justification.ts "decision-name" << HEREDOC -Multi-line -justification content -goes here -HEREDOC -``` - -## File format - -Files are created as: `{three-word-id}-{slug}.md` - -Example: `swift-emerald-river-decision-name.md` - -The file includes frontmatter: - -```markdown ---- -date: 2026-01-13 -title: Decision Name ---- - -Your content here -``` - -## When to use me - -Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization. diff --git a/.agents/skills/create-plan/SKILL.md b/.agents/skills/create-plan/SKILL.md deleted file mode 100644 index 8ceb2ef8c..000000000 --- a/.agents/skills/create-plan/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: create-plan -description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title -license: MIT -compatibility: opencode -metadata: - audience: agents - workflow: planning ---- - -## What I do - -I help you create new plan files in the `.agents/plans/` directory. Each plan file gets: - -- A unique three-word identifier (e.g., `happy-blue-moon`) -- Frontmatter with the current date and formatted title -- Content you provide - -## How to use - -Run the script with a slug and content: - -```bash -npx tsx scripts/create-plan.ts "feature-name" "Plan content here" -``` - -Or use heredoc for multi-line content: - -```bash -npx tsx scripts/create-plan.ts "feature-name" << HEREDOC -Multi-line -plan content -goes here -HEREDOC -``` - -## File format - -Files are created as: `{three-word-id}-{slug}.md` - -Example: `happy-blue-moon-feature-name.md` - -The file includes frontmatter: - -```markdown ---- -date: 2026-01-13 -title: Feature Name ---- - -Your content here -``` - -## When to use me - -Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization. diff --git a/.agents/skills/create-scratch/SKILL.md b/.agents/skills/create-scratch/SKILL.md deleted file mode 100644 index e44e4779d..000000000 --- a/.agents/skills/create-scratch/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: create-scratch -description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title -license: MIT -compatibility: opencode -metadata: - audience: agents - workflow: exploration ---- - -## What I do - -I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets: - -- A unique three-word identifier (e.g., `calm-teal-cloud`) -- Frontmatter with the current date and formatted title -- Content you provide - -## How to use - -Run the script with a slug and content: - -```bash -npx tsx scripts/create-scratch.ts "note-name" "Scratch content here" -``` - -Or use heredoc for multi-line content: - -```bash -npx tsx scripts/create-scratch.ts "note-name" << HEREDOC -Multi-line -scratch content -goes here -HEREDOC -``` - -## File format - -Files are created as: `{three-word-id}-{slug}.md` - -Example: `calm-teal-cloud-note-name.md` - -The file includes frontmatter: - -```markdown ---- -date: 2026-01-13 -title: Note Name ---- - -Your content here -``` - -## When to use me - -Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization. diff --git a/.env.example b/.env.example index f723e1c66..5f3da7c1f 100644 --- a/.env.example +++ b/.env.example @@ -48,7 +48,7 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" # [[SIGNING]] -# The transport to use for document signing. Available options: local (default) | gcloud-hsm +# The transport to use for document signing. Available options: local (default) | gcloud-hsm | csc NEXT_PRIVATE_SIGNING_TRANSPORT="local" # OPTIONAL: The passphrase to use for the local file-based signing transport. NEXT_PRIVATE_SIGNING_PASSPHRASE= @@ -70,6 +70,14 @@ NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH= NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS= # OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport. NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH= +# OPTIONAL: The base URL of the Cloud Signature Consortium (CSC) provider for the csc signing transport. +NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL= +# OPTIONAL: The OAuth client ID registered with the CSC provider for the csc signing transport. +NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID= +# OPTIONAL: The OAuth client secret registered with the CSC provider for the csc signing transport. +NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET= +# OPTIONAL: Default signature level for envelopes created on a CSC instance when the caller doesn't specify one. Available options: AES (default) | QES. Explicit AES/QES requests always pass through unchanged. +NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL= # OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps). NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY= # OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL. @@ -95,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" @@ -172,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/.eslintignore b/.eslintignore deleted file mode 100644 index b7f7e638f..000000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -# Config files -*.config.js -*.config.cjs - -# Statically hosted javascript files -apps/*/public/*.js -apps/*/public/*.cjs -scripts/ diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 455860ea1..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,16 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - extends: ['@documenso/eslint-config'], - rules: { - '@next/next/no-img-element': 'off', - 'no-unreachable': 'error', - 'react-hooks/exhaustive-deps': 'off', - }, - settings: { - next: { - rootDir: ['apps/*/'], - }, - }, - ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'], -}; 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/ci.yml b/.github/workflows/ci.yml index beb670c01..a62fbd480 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Build App runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 @@ -36,6 +37,7 @@ jobs: build_docker: name: Build Docker Image runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d74f30387..b5ac9017a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -11,6 +11,7 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest + timeout-minutes: 60 permissions: actions: read contents: read diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 96d3d8ce2..6df588caa 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,7 @@ jobs: deploy: if: github.repository == 'documenso/documenso' runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout code diff --git a/.github/workflows/first-interaction.yml b/.github/workflows/first-interaction.yml index d74b4f9d1..dd2437be8 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 974fda86d..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.repository == 'documenso/documenso' && ${{ 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/issue-labeler.yml b/.github/workflows/issue-labeler.yml index 07e2f6983..2ad688094 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -8,6 +8,7 @@ jobs: label-when-assigned: if: github.repository == 'documenso/documenso' runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Label issue uses: actions/github-script@v6 diff --git a/.github/workflows/issue-opened.yml b/.github/workflows/issue-opened.yml index f9166587c..948d0d48d 100644 --- a/.github/workflows/issue-opened.yml +++ b/.github/workflows/issue-opened.yml @@ -8,6 +8,7 @@ jobs: label_issues: if: github.repository == 'documenso/documenso' runs-on: ubuntu-latest + timeout-minutes: 10 permissions: issues: write steps: diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index d243b1335..4d6d80ca3 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -14,6 +14,7 @@ jobs: contents: read pull-requests: write runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/labeler@v4 with: diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml deleted file mode 100644 index b334024fe..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.repository == 'documenso/documenso' && ${{ 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/publish.yml b/.github/workflows/publish.yml index f33295aed..907a43351 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,6 +15,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Build and publish platform containers runs-on: ${{ matrix.os }} + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -80,6 +81,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Create and publish manifest runs-on: ubuntu-latest + timeout-minutes: 60 needs: build_and_publish_platform_containers steps: - name: Checkout diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index 82996cf28..a791e2ae5 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -16,25 +16,8 @@ jobs: if: github.repository == 'documenso/documenso' name: Validate PR title runs-on: ubuntu-latest + timeout-minutes: 10 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: @@ -45,8 +28,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: @@ -54,10 +35,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 0771e5178..a93ff4aa7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,6 +8,7 @@ jobs: stale: if: github.repository == 'documenso/documenso' runs-on: ubuntu-latest + timeout-minutes: 10 permissions: issues: write pull-requests: write @@ -22,4 +23,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/.github/workflows/translations-force-pull.yml b/.github/workflows/translations-force-pull.yml index 09f6b4625..4d5f24279 100644 --- a/.github/workflows/translations-force-pull.yml +++ b/.github/workflows/translations-force-pull.yml @@ -19,6 +19,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Force pull translations runs-on: ubuntu-latest + timeout-minutes: 10 environment: Translations permissions: contents: write diff --git a/.github/workflows/translations-pull.yml b/.github/workflows/translations-pull.yml index e7cf438bd..93d9616b5 100644 --- a/.github/workflows/translations-pull.yml +++ b/.github/workflows/translations-pull.yml @@ -17,6 +17,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Pull translations runs-on: ubuntu-latest + timeout-minutes: 10 environment: Translations permissions: contents: write diff --git a/.github/workflows/translations-upload.yml b/.github/workflows/translations-upload.yml index f09c5c140..0e48d6396 100644 --- a/.github/workflows/translations-upload.yml +++ b/.github/workflows/translations-upload.yml @@ -15,6 +15,7 @@ jobs: if: github.repository == 'documenso/documenso' name: Extract and upload translations runs-on: ubuntu-latest + timeout-minutes: 30 environment: Translations permissions: contents: write 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/.prettierignore b/.prettierignore deleted file mode 100644 index f5c70c1d5..000000000 --- a/.prettierignore +++ /dev/null @@ -1,20 +0,0 @@ -node_modules -.next -public -**/**/node_modules -**/**/.next -**/**/public -packages/lib/translations/**/*.js - -*.lock -*.log -*.test.ts - -.gitignore -.npmignore -.prettierignore -.DS_Store -.eslintignore - -# Docs MDX - Prettier strips indentation from code blocks inside components -apps/docs/content/**/*.mdx 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 -Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml +# Report security vulnerabilities privately via GitHub Security Advisories (preferred). +Contact: https://github.com/documenso/documenso/security/advisories/new -# Report critical issues privately to let us take appropriate action before publishing. +# 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 + 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/ARCHITECTURE.md b/ARCHITECTURE.md index be9dbb555..d3cee2f37 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -42,8 +42,8 @@ Documenso is an open-source document signing platform built as a **monorepo** us | Package | Description | Port | | -------------------------- | -------------------------------------------------------- | ---- | | `@documenso/remix` | Main application - React Router (Remix) with Hono server | 3000 | -| `@documenso/documentation` | Documentation site (Next.js + Nextra) | 3002 | | `@documenso/openpage-api` | Public analytics API | 3003 | +| `@documenso/docs` | Documentation site | 3004 | ### Core Packages (`packages/`) 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/DEVALOK_FORK_NOTES.md b/DEVALOK_FORK_NOTES.md index f6c165708..5e309da0f 100644 --- a/DEVALOK_FORK_NOTES.md +++ b/DEVALOK_FORK_NOTES.md @@ -10,16 +10,37 @@ It is **not** a real GitHub fork — Railway's eject pushed a squashed initial c |---|---|---| | 2026-05-04 | (initial Railway eject) | Squashed initial commit `af5d802` | | 2026-06-04 | `0ecde7a` | First upstream sync. 86 commits absorbed (~v2.10.x → v2.11.x territory). 4 additive Prisma migrations applied. Guards re-applied to all 14 upstream-only workflows. | +| 2026-08-19 | `7533016` (v2.17.0) | Second upstream sync. 120 commits, v2.11 → v2.17.0. 4 additive Prisma migrations. **Motivation:** deployed commit predated upstream `583e35c7` ("fix: ensures new expire on setSessionCookie", #2708), which froze every session cookie's `Expires` at process-start + 30 days — all logins silently broke on 2026-07-04. Guards re-applied to 13 workflows (upstream deleted `issue-assignee-check.yml` and `pr-review-reminder.yml`). | ## Upstream sync +Histories are **unrelated** (squashed eject), so `git merge upstream/main` fails with +`refusing to merge unrelated histories`. Every sync is a squash-import of upstream's tree. + +Import the tree wholesale — do **not** hand-apply a diff. The 2026-06-04 sync applied upstream's +additions and modifications but silently kept files upstream had **deleted** (`packages/eslint-config/`, +`packages/prettier-config/`, `prettier.config.cjs`, `.prettierignore`, `.eslintrc.cjs`, +`packages/lib/server-only/document/send-completed-email.ts`, stale embedding docs). `read-tree` propagates +deletions; a diff does not. + ```bash git remote add upstream https://github.com/documenso/documenso.git # one-time git fetch upstream -git merge upstream/main -git push origin main +git checkout -b chore/upstream-merge- + +# fork-only delta = workflow guards + this file. Capture it against the PREVIOUS sync base. +git diff HEAD -- .github/workflows DEVALOK_FORK_NOTES.md > ../fork-delta.patch + +# working tree becomes upstream/main exactly (adds, mods AND deletes) +git read-tree -u --reset upstream/main + +# re-apply fork guards; --exclude any workflow upstream has since deleted +git apply -3 --index ../fork-delta.patch ``` +Then audit guard coverage per job before pushing (see below), and check +`git diff upstream/main -- packages/prisma/migrations` for pending migrations. + ## CI / workflow conventions Every workflow that depends on upstream-only secrets or infrastructure (Crowdin, DockerHub, Warp runners, ghcr.io/documenso, upstream community automation) is **guarded** with: @@ -37,6 +58,29 @@ This is intentional. The workflows are left in place so upstream merges stay cle 1. Diff workflow changes: `git diff HEAD~1 .github/workflows/` 2. If upstream added a new job that depends on their secrets/infra, add the same `if:` guard before pushing. 3. If upstream added a new `.yml` file that's entirely upstream-only, add the guard to every job in it. +4. Audit every job, not every file — a guarded file can still gain an unguarded job: + +```bash +python - <<'EOF' +import re, glob +for f in sorted(glob.glob('.github/workflows/*.yml')): + lines = open(f, encoding='utf-8').read().splitlines() + i = next((n for n, l in enumerate(lines) if l.rstrip() == 'jobs:'), None) + if i is None: continue + cur = None + print(f) + for l in lines[i + 1:]: + m = re.match(r'^ ([A-Za-z0-9_-]+):\s*$', l) + if m: + cur = {'n': m.group(1), 'g': False} + print(' ', cur) + continue + if cur and "github.repository == 'documenso/documenso'" in l: + print(' ^ guarded') +EOF +``` + +Expected open (intentional): `ci.yml/build_docker`, `codeql-analysis.yml/analyze`. Everything else guarded. ### Currently guarded workflows @@ -46,11 +90,9 @@ This is intentional. The workflows are left in place so upstream merges stay cle | `deploy.yml` | Pushes `main` → `release` using upstream `GH_TOKEN` | | `e2e-tests.yml` | Uses `warp-ubuntu-*` runners only available to upstream | | `first-interaction.yml` | Welcome message linking to upstream Discord | -| `issue-assignee-check.yml` | Upstream community management | | `issue-labeler.yml` | Upstream issue triage | | `issue-opened.yml` | Upstream issue triage | | `pr-labeler.yml` | Upstream PR triage | -| `pr-review-reminder.yml` | Upstream reviewer reminder | | `publish.yml` | Publishes to `documenso/*` on DockerHub + `ghcr.io/documenso` | | `semantic-pull-requests.yml` | Conventional-commit PR title enforcement | | `stale.yml` | Upstream community issue/PR stale-bot | diff --git a/README.md b/README.md index f5117abd0..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 @@ -182,7 +188,7 @@ For full instructions, requirements, and configuration details, see the [Self Ho #### Railway -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/bG6D4p) +[![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 @@ -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 cb719ffb8..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.app/button.svg)](https://railway.app/template/DjrRRX) - -## 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/api/documents.mdx b/apps/docs/content/docs/developers/api/documents.mdx index a21a2740b..bd526e828 100644 --- a/apps/docs/content/docs/developers/api/documents.mdx +++ b/apps/docs/content/docs/developers/api/documents.mdx @@ -6,6 +6,8 @@ description: Create, manage, and send documents for signing via the API. import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). @@ -26,35 +28,62 @@ Each document contains one or more PDF files, a list of recipients, and the fiel A document object contains the following properties: -| Property | Type | Description | -| --------------- | -------------- | -------------------------------------------------------------- | -| `id` | string | Unique identifier (e.g., `envelope_abc123`) | -| `type` | string | `DOCUMENT` or `TEMPLATE` | -| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, or `REJECTED` | -| `title` | string | Document title | -| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `API` | -| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` | -| `externalId` | string \| null | Your custom identifier for the document | -| `createdAt` | string | ISO 8601 timestamp | -| `updatedAt` | string | ISO 8601 timestamp | -| `completedAt` | string \| null | Timestamp when all recipients completed signing | -| `deletedAt` | string \| null | Timestamp if soft-deleted | -| `recipients` | array | List of recipients and their signing status | -| `fields` | array | Signature and form fields on the document | -| `envelopeItems` | array | PDF files attached to the document | -| `documentMeta` | object | Email settings, redirect URL, signing options | +| Property | Type | Description | +| ------------------- | -------------- | -------------------------------------------------------------------------------------------- | +| `id` | string | Unique identifier (e.g., `envelope_abc123`) | +| `secondaryId` | string | Legacy identifier in prefixed form (`document_123` for documents, `template_123` for templates) | +| `internalVersion` | number | Internal envelope schema version | +| `type` | string | `DOCUMENT` or `TEMPLATE` | +| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, or `CANCELLED` | +| `title` | string | Document title | +| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | +| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` | +| `templateType` | string | Template visibility: `PUBLIC`, `PRIVATE`, or `ORGANISATION` (only meaningful for templates) | +| `externalId` | string \| null | Your custom identifier for the document | +| `userId` | number | ID of the user who owns the document | +| `teamId` | number | ID of the team the document belongs to | +| `folderId` | string \| null | ID of the folder containing the document | +| `templateId` | number \| null | Legacy ID of the template this document was created from | +| `authOptions` | object \| null | Access and action authentication requirements | +| `formValues` | object \| null | Pre-filled form values | +| `publicTitle` | string | Public title shown on profile and direct-link pages | +| `publicDescription` | string | Public description shown on profile and direct-link pages | +| `createdAt` | string | ISO 8601 timestamp | +| `updatedAt` | string | ISO 8601 timestamp | +| `completedAt` | string \| null | Timestamp when all recipients completed signing | +| `deletedAt` | string \| null | Timestamp if soft-deleted | +| `recipients` | array | List of recipients and their signing status | +| `fields` | array | Signature and form fields on the document | +| `envelopeItems` | array | PDF files attached to the document | +| `directLink` | object \| null | Direct-link signing configuration (`id`, `token`, `enabled`, `directTemplateRecipientId`) | +| `team` | object | Owning team (`id`, `url`) | +| `user` | object | Document owner (`id`, `name`, `email`) | +| `documentMeta` | object | Email settings, redirect URL, signing options | + +Documents created through the API have `source: "DOCUMENT"` — there is no separate `API` source value. To tag documents created by your integration, set `externalId` when creating them. ### Example Document Object ```json { "id": "envelope_abc123xyz", + "secondaryId": "document_123", + "internalVersion": 2, "type": "DOCUMENT", "status": "PENDING", - "source": "API", + "source": "DOCUMENT", "visibility": "EVERYONE", + "templateType": "PRIVATE", "title": "Service Agreement", "externalId": "contract-2025-001", + "userId": 1, + "teamId": 1, + "folderId": null, + "templateId": null, + "authOptions": null, + "formValues": null, + "publicTitle": "", + "publicDescription": "", "createdAt": "2025-01-15T10:30:00.000Z", "updatedAt": "2025-01-15T10:35:00.000Z", "completedAt": null, @@ -71,23 +100,41 @@ A document object contains the following properties: ], "fields": [ { - "id": "field_123", + "id": 123, + "secondaryId": "field_abc123", "type": "SIGNATURE", + "recipientId": 1, + "envelopeId": "envelope_abc123xyz", + "envelopeItemId": "envelope_item_xyz", "page": 1, - "positionX": 10, - "positionY": 80, - "width": 30, - "height": 5, - "recipientId": 1 + "positionX": "10", + "positionY": "80", + "width": "30", + "height": "5", + "customText": "", + "inserted": false, + "fieldMeta": null } ], "envelopeItems": [ { "id": "envelope_item_xyz", + "envelopeId": "envelope_abc123xyz", + "documentDataId": "doc_data_abc123", "title": "contract.pdf", "order": 1 } ], + "directLink": null, + "team": { + "id": 1, + "url": "your-team" + }, + "user": { + "id": 1, + "name": "Jane Smith", + "email": "jane@example.com" + }, "documentMeta": { "subject": "Please sign this document", "message": "Hi, please review and sign this agreement.", @@ -97,6 +144,8 @@ A document object contains the following properties: } ``` +Field position and size values are stored as decimals and serialized as strings in API responses. + ## List Documents Retrieve a paginated list of documents. @@ -112,7 +161,7 @@ GET /envelope | `page` | integer | Page number (default: 1) | | `perPage` | integer | Results per page (default: 10, max: 100) | | `type` | string | Filter by `DOCUMENT` or `TEMPLATE` | -| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | +| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | | `source` | string | Filter by creation source | | `folderId` | string | Filter by folder ID | | `orderByColumn` | string | Sort field (only `createdAt` supported) | @@ -152,8 +201,8 @@ const response = await fetch(`${BASE_URL}/envelope`, { }, }); -const { data, pagination } = await response.json(); -console.log(`Found ${pagination.totalItems} documents`); +const { data, count } = await response.json(); +console.log(`Found ${count} documents`); // Filter by status const pendingResponse = await fetch( @@ -195,12 +244,10 @@ const pendingDocs = await pendingResponse.json(); ] } ], - "pagination": { - "page": 1, - "perPage": 10, - "totalPages": 5, - "totalItems": 42 - } + "count": 42, + "currentPage": 1, + "perPage": 10, + "totalPages": 5 } ``` @@ -626,6 +673,72 @@ The response includes signing URLs for each recipient: --- +## Cancel Document + +Cancel a pending document. This changes its status from `PENDING` to `CANCELLED`. + +``` +POST /envelope/cancel +``` + +### Request Body + +| Field | Type | Required | Description | +| ------------ | ------ | -------- | ----------------------------------- | +| `envelopeId` | string | Yes | Document ID | +| `reason` | string | No | Reason for cancelling the document | + +### Code Examples + + + +```bash +curl -X POST "https://app.documenso.com/api/v2/envelope/cancel" \ + -H "Authorization: api_xxxxxxxxxxxxxxxx" \ + -H "Content-Type: application/json" \ + -d '{ + "envelopeId": "envelope_abc123", + "reason": "The agreement is no longer needed." + }' +``` + + +```typescript +const response = await fetch('https://app.documenso.com/api/v2/envelope/cancel', { + method: 'POST', + headers: { + Authorization: 'api_xxxxxxxxxxxxxxxx', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + envelopeId: 'envelope_abc123', + reason: 'The agreement is no longer needed.', + }), +}); + +const { success } = await response.json(); +``` + + + +### Response + +```json +{ + "success": true +} +``` + +### Behavior + +- Only documents in `PENDING` status can be cancelled. Other statuses return `400`. +- Cancellation is not idempotent. Cancelling the same document again returns `400`. +- The document owner and team members with `MANAGER` or higher permissions can cancel it. Requests for documents you cannot view return `404`; requests for visible documents without sufficient permissions return `401`. +- A successful cancellation fires the `DOCUMENT_CANCELLED` webhook. +- Cancellation emails are sent only to eligible non-CC, non-rejected recipients who were sent or opened the document. + +--- + ## Delete Document Delete a document. Completed documents cannot be deleted. @@ -668,7 +781,7 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/delete', const { success } = await response.json(); -```` +``` @@ -678,7 +791,7 @@ const { success } = await response.json(); { "success": true } -```` +``` --- @@ -692,9 +805,11 @@ POST /envelope/get-many ### Request Body -| Field | Type | Required | Description | -| ------------- | ----- | -------- | --------------------- | -| `envelopeIds` | array | Yes | Array of document IDs | +| Field | Type | Required | Description | +| ---------- | ------ | -------- | ---------------------------------------------------------------------------- | +| `ids` | object | Yes | ID selector containing `type` and `ids` | +| `ids.type` | string | Yes | `envelopeId`, `documentId`, or `templateId` | +| `ids.ids` | array | Yes | 1-20 IDs: strings for `envelopeId`; numbers for `documentId` or `templateId` | ### Code Examples @@ -705,12 +820,17 @@ curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \ -H "Authorization: api_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ - "envelopeIds": ["envelope_abc123", "envelope_def456", "envelope_ghi789"] + "ids": { + "type": "envelopeId", + "ids": ["envelope_abc123", "envelope_def456", "envelope_ghi789"] + } }' ``` ```typescript +const requestedIds = ['envelope_abc123', 'envelope_def456', 'envelope_ghi789']; + const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', { method: 'POST', headers: { @@ -718,16 +838,36 @@ const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many 'Content-Type': 'application/json', }, body: JSON.stringify({ - envelopeIds: ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'], + ids: { + type: 'envelopeId', + ids: requestedIds, + }, }), }); -const documents = await response.json(); +const { data } = await response.json(); -```` +``` +### Response + +```json +{ + "data": [ + { + "id": "envelope_abc123", + "type": "DOCUMENT", + "status": "PENDING", + "title": "Service Agreement" + } + ] +} +``` + +The endpoint silently omits envelopes you cannot access instead of returning `404`. Compare `data.length` with `requestedIds.length` to detect omissions. + --- ## Document Statuses @@ -738,6 +878,7 @@ const documents = await response.json(); | `PENDING` | Document has been sent. Waiting for recipients to sign. | | `COMPLETED` | All recipients have signed. Document is sealed. | | `REJECTED` | A recipient rejected the document. | +| `CANCELLED` | The document was cancelled by its owner or a team member with `MANAGER` or higher permissions. | ### Status Transitions @@ -745,11 +886,13 @@ const documents = await response.json(); flowchart LR DRAFT --> PENDING --> COMPLETED PENDING --> REJECTED + PENDING --> CANCELLED ``` - **DRAFT to PENDING**: Call the distribute endpoint - **PENDING to COMPLETED**: All recipients complete their signing - **PENDING to REJECTED**: A recipient rejects the document +- **PENDING to CANCELLED**: The document owner or a team member with `MANAGER` or higher permissions cancels the document You cannot modify recipients or fields after a document moves to `PENDING` status. @@ -771,8 +914,8 @@ flowchart LR | Parameter | Values | Description | | ---------- | ------------------------------------------- | ------------------------- | | `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type | -| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | Filter by status | -| `source` | `DOCUMENT`, `TEMPLATE`, `API` | Filter by creation source | +| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | Filter by status | +| `source` | `DOCUMENT`, `TEMPLATE`, `TEMPLATE_DIRECT_LINK` | Filter by creation source | | `folderId` | string | Filter by folder | ### Sorting @@ -798,10 +941,10 @@ async function getAllPendingDocuments() { }, ); - const { data, pagination } = await response.json(); + const { data, currentPage, totalPages } = await response.json(); documents.push(...data); - hasMore = page < pagination.totalPages; + hasMore = currentPage < totalPages; page++; } diff --git a/apps/docs/content/docs/developers/api/index.mdx b/apps/docs/content/docs/developers/api/index.mdx index 7f446c7ad..e8d7139eb 100644 --- a/apps/docs/content/docs/developers/api/index.mdx +++ b/apps/docs/content/docs/developers/api/index.mdx @@ -5,6 +5,8 @@ description: Complete reference for the Documenso REST API. import { Callout } from 'fumadocs-ui/components/callout'; + + The guides below cover common API patterns but may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). diff --git a/apps/docs/content/docs/developers/api/meta.json b/apps/docs/content/docs/developers/api/meta.json index 7a19089dd..7906bbe97 100644 --- a/apps/docs/content/docs/developers/api/meta.json +++ b/apps/docs/content/docs/developers/api/meta.json @@ -8,6 +8,7 @@ "teams", "rate-limits", "versioning", + "migrate-to-envelopes", "developer-mode", "common-errors" ] diff --git a/apps/docs/content/docs/developers/api/migrate-to-envelopes.mdx b/apps/docs/content/docs/developers/api/migrate-to-envelopes.mdx new file mode 100644 index 000000000..5a719e90c --- /dev/null +++ b/apps/docs/content/docs/developers/api/migrate-to-envelopes.mdx @@ -0,0 +1,249 @@ +--- +title: Migrating to Envelopes +description: Why Documenso unified documents and templates into envelopes, and how to migrate from the deprecated document and template create endpoints. +--- + +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +## Summary + +The following items have been deprecated and will be removed on the 1st of March 2027: + +- API V1 +- A subset of SDK/API V2 endpoints +- Legacy documents and templates +- EmbedCreateDocumentV1 +- EmbedCreateTemplateV1 +- EmbedUpdateDocumentV1 +- EmbedUpdateTemplateV1 + +The beta endpoint `/api/v2-beta` will also be removed. Use `/api/v2` instead, which is a drop-in replacement. + +Nothing breaks before 1st of March 2027, so you can migrate at your own pace. + +## What are legacy documents and templates + +These are documents and templates created by the following endpoints: + +- `POST /api/v2/document/create` +- `POST /api/v2/document/create/beta` +- `POST /api/v2/template/create` +- `POST /api/v2/template/create/beta` +- `POST /api/v1/documents` +- `POST /api/v1/templates` +- `POST /api/v1/templates/create-document` +- `POST /api/v1/templates/generate-document` + +## What replaces legacy documents and templates + +At the end of 2025 we introduced a unified system for documents and templates, called envelopes. + +We still reference documents and templates throughout the documentation and application to distinguish them, but internally they are envelopes. + +Moving to the envelope system gives you: + +- **Multiple PDFs in one envelope.** Send several documents to sign in a single request. +- **One API for documents and templates.** Learn one set of endpoints instead of two misaligned ones. +- **A better editor and signing experience** for you and your recipients. + +## How to migrate + +{/* prettier-ignore */} + + + ### Switch to the envelope endpoints + + Replace each deprecated endpoint with its `/api/v2/envelope/*` equivalent from the [mapping tables](#endpoint-mapping-reference) below. + + + ### Set the envelope `type` on create + + A single endpoint, `POST /api/v2/envelope/create`, can create both documents and templates. Set `type` to `DOCUMENT` or `TEMPLATE`. You can now upload more than one PDF using the `files` field. + + + ### Update how you store IDs + + Envelope IDs are **strings** (for example `envelope_abc123`), not numbers. Update any code that stores, parses, or compares IDs. + + + ### Test, then remove the old calls + + Verify the new flow against your account, then delete the deprecated calls. + + + +The main data differences are as follows: +- ID format changed from number to string (e.g. `42` to `envelope_abc123`) +- pageNumber becomes page +- pageX becomes positionX +- pageY becomes positionY + +See the [Documents API](/docs/developers/api/documents) and [Templates API](/docs/developers/api/templates) for the full envelope reference. + +### Deprecated V1 API Endpoints + +Full reference in the [V1 OpenAPI reference](https://openapi-v1.documenso.com). + +| Deprecated endpoint | Replacement | +| -------------------------------------------------------- | ----------------------------------------------------- | +| `GET /api/v1/documents` | `GET /api/v2/envelope` | +| `GET /api/v1/documents/{id}` | `GET /api/v2/envelope/{envelopeId}` | +| `POST /api/v1/documents` | `POST /api/v2/envelope/create` | +| `POST /api/v1/documents/{id}/send` | `POST /api/v2/envelope/distribute` | +| `POST /api/v1/documents/{id}/resend` | `POST /api/v2/envelope/redistribute` | +| `DELETE /api/v1/documents/{id}` | `POST /api/v2/envelope/delete` | +| `GET /api/v1/documents/{id}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | +| `POST /api/v1/documents/{id}/recipients` | `POST /api/v2/envelope/recipient/create-many` | +| `PATCH /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/update-many` | +| `DELETE /api/v1/documents/{id}/recipients/{recipientId}` | `POST /api/v2/envelope/recipient/delete` | +| `POST /api/v1/documents/{id}/fields` | `POST /api/v2/envelope/field/create-many` | +| `PATCH /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/update-many` | +| `DELETE /api/v1/documents/{id}/fields/{fieldId}` | `POST /api/v2/envelope/field/delete` | +| `GET /api/v1/templates` | `GET /api/v2/envelope` (with `type=TEMPLATE`) | +| `GET /api/v1/templates/{id}` | `GET /api/v2/envelope/{envelopeId}` | +| `POST /api/v1/templates` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | +| `DELETE /api/v1/templates/{id}` | `POST /api/v2/envelope/delete` | +| `POST /api/v1/templates/{templateId}/create-document` | `POST /api/v2/envelope/use` | +| `POST /api/v1/templates/{templateId}/generate-document` | `POST /api/v2/envelope/use` | + +### Deprecated V2 API Endpoints + +Full reference in the [V2 OpenAPI reference](https://openapi.documenso.com). + +#### Documents + +| Deprecated endpoint | Replacement | +| ------------------------------------------------- | ----------------------------------------------------- | +| `GET /api/v2/document` | `GET /api/v2/envelope` | +| `GET /api/v2/document/{documentId}` | `GET /api/v2/envelope/{envelopeId}` | +| `POST /api/v2/document/get-many` | `POST /api/v2/envelope/get-many` (body changes from `documentIds: number[]` to `ids: { type: "documentId"; ids: number[] }`) | +| `POST /api/v2/document/create` | `POST /api/v2/envelope/create` | +| `POST /api/v2/document/create/beta` | `POST /api/v2/envelope/create` | +| `POST /api/v2/document/update` | `POST /api/v2/envelope/update` | +| `POST /api/v2/document/delete` | `POST /api/v2/envelope/delete` | +| `POST /api/v2/document/duplicate` | `POST /api/v2/envelope/duplicate` | +| `POST /api/v2/document/distribute` | `POST /api/v2/envelope/distribute` | +| `POST /api/v2/document/redistribute` | `POST /api/v2/envelope/redistribute` | +| `GET /api/v2/document/attachment` | `GET /api/v2/envelope/attachment` | +| `POST /api/v2/document/attachment/create` | `POST /api/v2/envelope/attachment/create` | +| `POST /api/v2/document/attachment/update` | `POST /api/v2/envelope/attachment/update` | +| `POST /api/v2/document/attachment/delete` | `POST /api/v2/envelope/attachment/delete` | +| `GET /api/v2/document/{documentId}/download` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | +| `GET /api/v2/document/{documentId}/download-beta` | `GET /api/v2/envelope/item/{envelopeItemId}/download` | + +#### Templates + +| Deprecated endpoint | Replacement | +| ------------------------------------- | ------------------------------------------------ | +| `GET /api/v2/template` | `GET /api/v2/envelope` (with `type=TEMPLATE`) | +| `GET /api/v2/template/{templateId}` | `GET /api/v2/envelope/{envelopeId}` | +| `POST /api/v2/template/get-many` | `POST /api/v2/envelope/get-many` (body changes from `templateIds: number[]` to `ids: { type: "templateId"; ids: number[] }`) | +| `POST /api/v2/template/create` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | +| `POST /api/v2/template/create/beta` | `POST /api/v2/envelope/create` (`type=TEMPLATE`) | +| `POST /api/v2/template/update` | `POST /api/v2/envelope/update` | +| `POST /api/v2/template/duplicate` | `POST /api/v2/envelope/duplicate` | +| `POST /api/v2/template/delete` | `POST /api/v2/envelope/delete` | +| `POST /api/v2/template/use` | `POST /api/v2/envelope/use` | +| `POST /api/v2/template/direct/create` | **Pending replacement** | +| `POST /api/v2/template/direct/delete` | **Pending replacement** | +| `POST /api/v2/template/direct/toggle` | **Pending replacement** | + +#### Document fields + +| Deprecated endpoint | Replacement | +| ----------------------------------------- | ----------------------------------------- | +| `GET /api/v2/document/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` | +| `POST /api/v2/document/field/create` | `POST /api/v2/envelope/field/create-many` | +| `POST /api/v2/document/field/create-many` | `POST /api/v2/envelope/field/create-many` | +| `POST /api/v2/document/field/update` | `POST /api/v2/envelope/field/update-many` | +| `POST /api/v2/document/field/update-many` | `POST /api/v2/envelope/field/update-many` | +| `POST /api/v2/document/field/delete` | `POST /api/v2/envelope/field/delete` | + +#### Template fields + +| Deprecated endpoint | Replacement | +| ----------------------------------------- | ----------------------------------------- | +| `GET /api/v2/template/field/{fieldId}` | `GET /api/v2/envelope/field/{fieldId}` | +| `POST /api/v2/template/field/create` | `POST /api/v2/envelope/field/create-many` | +| `POST /api/v2/template/field/create-many` | `POST /api/v2/envelope/field/create-many` | +| `POST /api/v2/template/field/update` | `POST /api/v2/envelope/field/update-many` | +| `POST /api/v2/template/field/update-many` | `POST /api/v2/envelope/field/update-many` | +| `POST /api/v2/template/field/delete` | `POST /api/v2/envelope/field/delete` | + +#### Document recipients + +| Deprecated endpoint | Replacement | +| ---------------------------------------------- | ---------------------------------------------- | +| `GET /api/v2/document/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` | +| `POST /api/v2/document/recipient/create` | `POST /api/v2/envelope/recipient/create-many` | +| `POST /api/v2/document/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` | +| `POST /api/v2/document/recipient/update` | `POST /api/v2/envelope/recipient/update-many` | +| `POST /api/v2/document/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` | +| `POST /api/v2/document/recipient/delete` | `POST /api/v2/envelope/recipient/delete` | + +#### Template recipients + +| Deprecated endpoint | Replacement | +| ---------------------------------------------- | ---------------------------------------------- | +| `GET /api/v2/template/recipient/{recipientId}` | `GET /api/v2/envelope/recipient/{recipientId}` | +| `POST /api/v2/template/recipient/create` | `POST /api/v2/envelope/recipient/create-many` | +| `POST /api/v2/template/recipient/create-many` | `POST /api/v2/envelope/recipient/create-many` | +| `POST /api/v2/template/recipient/update` | `POST /api/v2/envelope/recipient/update-many` | +| `POST /api/v2/template/recipient/update-many` | `POST /api/v2/envelope/recipient/update-many` | +| `POST /api/v2/template/recipient/delete` | `POST /api/v2/envelope/recipient/delete` | + +### Embedding components + +| Deprecated component | Replacement | +| ----------------------- | --------------------- | +| `EmbedCreateDocumentV1` | `EmbedCreateEnvelope` | +| `EmbedCreateTemplateV1` | `EmbedCreateEnvelope` | +| `EmbedUpdateDocumentV1` | `EmbedUpdateEnvelope` | +| `EmbedUpdateTemplateV1` | `EmbedUpdateEnvelope` | + +See the [embedding guide](/docs/developers/embedding) for the envelope components. + +## FAQ + + + + The deprecated V1 API, the V2 endpoints listed above, and the V1 embedding components are removed. + Requests to them will fail, so migrate to the envelope API before that date. + + + Yes. Documents and templates you already created remain in your account and continue to work. They will automatically be converted to envelopes. Only + the deprecated endpoints you call are going away. Your data is not deleted. + + + No. Authentication is unchanged. The same API token works for the envelope endpoints under + `https://app.documenso.com/api/v2`. + + + Both are envelopes, distinguished by a `type` field of `DOCUMENT` or `TEMPLATE`. They share the same + endpoints, recipients, fields, and attachments. + + + The function calls to the legacy endpoints will break on the 1st of March 2027. Update to the latest SDK version and switch to its envelope methods. + The deprecated document and template methods map to the envelope endpoints in the tables above. + + + Reach out to [support@documenso.com](mailto:support@documenso.com) with your use case and we will + help you plan the migration. + + + +## Getting help + +- [V2 OpenAPI reference](https://openapi.documenso.com): the up-to-date envelope API. +- [V1 OpenAPI reference](https://openapi-v1.documenso.com): the deprecated V1 API. +- [support@documenso.com](mailto:support@documenso.com): migration questions and extensions. + +## See also + +- [Documents API](/docs/developers/api/documents): create and manage envelopes +- [Templates API](/docs/developers/api/templates): work with templates and direct links +- [Fields API](/docs/developers/api/fields) and [Recipients API](/docs/developers/api/recipients) +- [API Versioning](/docs/developers/api/versioning): how Documenso versions the public API diff --git a/apps/docs/content/docs/developers/api/rate-limits.mdx b/apps/docs/content/docs/developers/api/rate-limits.mdx index ecd50558e..878db97b6 100644 --- a/apps/docs/content/docs/developers/api/rate-limits.mdx +++ b/apps/docs/content/docs/developers/api/rate-limits.mdx @@ -11,10 +11,21 @@ Documenso enforces rate limits on all API endpoints to ensure service stability. ## HTTP Rate Limits -**Limit:** 100 requests per minute per IP address +The rate limit applies to: + +- `/api/v1/*` +- `/api/v2/*` +- `/api/v2-beta/*` + +**Limit:** 1000 requests per minute per IP address **Response:** 429 Too Many Requests -### Rate Limit Response + + This is the global per-IP ceiling. Your organisation may have its own rate limits configured below + this value, in which case you can be rate-limited before reaching the global limit. + + +### Global per-IP 429 Response ```json { @@ -22,10 +33,22 @@ Documenso enforces rate limits on all API endpoints to ensure service stability. } ``` - - No rate limit headers are currently provided. When you receive a 429 response, wait at least 60 - seconds before retrying. - +### Rate Limit Headers + +Responses from `/api/v1/*`, `/api/v2/*`, and `/api/v2-beta/*` include these headers. The only +exception is CORS preflight (`OPTIONS`) requests, which are answered before the rate limiter runs +and carry no rate limit headers: + +| Header | Description | +| ----------------------- | ---------------------------------------------------------------------- | +| `X-RateLimit-Limit` | Maximum requests allowed in the current global window | +| `X-RateLimit-Remaining` | Requests remaining in the current global window | +| `X-RateLimit-Reset` | End of the current global window, as a Unix epoch timestamp in seconds | + +A 429 response from a windowed limiter also includes `Retry-After`, in seconds, with a minimum +value of `1`. The global API limit uses fixed, epoch-aligned one-minute buckets, so the actual wait +until the next window is between 1 and 60 seconds. Honor `Retry-After` exactly instead of sleeping +for a fixed 60 seconds. See the [Retry-After handling example](/docs/developers/examples/common-workflows#error-handling-patterns). ## Resource Limits @@ -39,24 +62,55 @@ Beyond HTTP rate limits, your account has usage limits based on your subscriptio | Total Recipients | 10 | Unlimited | Unlimited | Unlimited | | Direct Templates | 3 | Unlimited | Unlimited | Unlimited | -### Error Response +### Organisation Limit 429 Responses + +Organisation windowed limits and organisation monthly quotas produce 429 responses whose body +shape depends on the API version, and neither matches the global per-IP limiter's +`{ "error": "..." }` body. -When you exceed a resource limit: +On `/api/v1/*`, the body contains only a message: ```json { - "error": "You have reached your document limit for this month. Please upgrade your plan.", - "code": "LIMIT_EXCEEDED", - "statusCode": 400 + "message": "Too many requests, please try again later. Contact support if you require higher limits." } ``` +On `/api/v2/*` and `/api/v2-beta/*`, the body is a structured error object: + +```json +{ + "message": "Too many requests, please try again later. Contact support if you require higher limits.", + "code": "TOO_MANY_REQUESTS", + "data": { + "code": "TOO_MANY_REQUESTS", + "httpStatus": 429, + "appError": { + "code": "TOO_MANY_REQUESTS", + "message": "Too many requests, please try again later. Contact support if you require higher limits." + } + } +} +``` + +Organisation windowed limit responses include the `X-RateLimit-*` headers and `Retry-After` for +their own window. Monthly quota responses carry no quota-specific rate limit headers or +`Retry-After` because the quota is not a time window; rely on the status code and message instead. + ## Error Codes -| Code | Status | Description | -| ------------------- | ------ | ----------------------------- | -| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded | -| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded | +| Code | Status | Description | +| ------------------- | ------ | ------------------------------------------------------------------ | +| `TOO_MANY_REQUESTS` | 429 | Global per-IP, organisation windowed, or monthly quota exceeded | +| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded | + +There are three sources of `TOO_MANY_REQUESTS` responses: + +1. The global per-IP limit, returning the `{ "error": "..." }` body shown above. +2. Organisation windowed rate limits for the `api`, `document`, and `email` counters. +3. Organisation monthly quotas for the same three counters. Every authenticated API request + consumes the `api` counter, so any endpoint can return this 429 once the monthly API quota is + exhausted — not just envelope-related ones. --- @@ -65,3 +119,4 @@ When you exceed a resource limit: - [Authentication](/docs/developers/getting-started/authentication) - API authentication guide - [API Versioning](/docs/developers/api/versioning) - API version management - [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API +- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Admins: set per-organisation resource quotas and rate limits (the HTTP rate limit above is separate and not admin-settable) diff --git a/apps/docs/content/docs/developers/api/templates.mdx b/apps/docs/content/docs/developers/api/templates.mdx index 8f5c5b667..b3f52e146 100644 --- a/apps/docs/content/docs/developers/api/templates.mdx +++ b/apps/docs/content/docs/developers/api/templates.mdx @@ -6,6 +6,8 @@ description: Create documents from reusable templates via API. import { Callout } from 'fumadocs-ui/components/callout'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com). diff --git a/apps/docs/content/docs/developers/api/versioning.mdx b/apps/docs/content/docs/developers/api/versioning.mdx index c137a869a..9e9435034 100644 --- a/apps/docs/content/docs/developers/api/versioning.mdx +++ b/apps/docs/content/docs/developers/api/versioning.mdx @@ -5,6 +5,8 @@ description: Versioning information for the Documenso public API. import { Callout } from 'fumadocs-ui/components/callout'; + + ## Overview Documenso uses API versioning to manage changes to the public API. This allows us to introduce new features, fix bugs, and make other changes without breaking existing integrations. @@ -19,7 +21,16 @@ Also, we may deprecate certain features or endpoints in the API. When we depreca --- +## Documents, Templates, and Envelopes + +Documenso has unified documents and templates into a single resource called an **envelope**. New integrations should create documents and templates through the `/envelope/*` endpoints. The `POST /document/create` and `POST /template/create` endpoints (including their `/beta` variants) are deprecated in favor of `POST /envelope/create`. + +See [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) for the rationale and step-by-step migration examples. + +--- + ## See Also +- [Migrating to the Envelope API](/docs/developers/api/migrate-to-envelopes) - Move from the document and template create endpoints - [Authentication](/docs/developers/getting-started/authentication) - API authentication guide - [Rate Limits](/docs/developers/api/rate-limits) - API rate limit details diff --git a/apps/docs/content/docs/developers/embedding/authoring/index.mdx b/apps/docs/content/docs/developers/embedding/authoring/index.mdx deleted file mode 100644 index 834d3cc71..000000000 --- a/apps/docs/content/docs/developers/embedding/authoring/index.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Authoring -description: Embed document, template, and envelope creation directly in your application. ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - -In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents, templates, and envelopes without leaving your application. - - - Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is - also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). - Contact sales for access. - - -## Versions - -Embedded authoring is available in two versions: - -- **[V1 Authoring](/docs/developers/embedding/authoring/v1)** — Works with V1 Documents and Templates. -- **[V2 Authoring](/docs/developers/embedding/authoring/v2)** — Works with Envelopes, which are the unified model for documents and templates. - -### Comparison - -| Aspect | V1 | V2 | -| --- | --- | --- | -| Entity model | Documents and Templates (separate) | Envelopes (unified, can be documents or templates) | -| API compatibility | V1 Documents/Templates API | V2 Envelopes API | -| Customization | 6 simple boolean flags | Rich structured settings with sections (general, settings, actions, envelope items, recipients) | - ---- - -## Presign Tokens - -Before using any authoring component, obtain a presign token from your backend: - -``` -POST /api/v2/embedding/create-presign-token -``` - -This endpoint requires your Documenso API key. The token has a default expiration of 1 hour. - -See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details. - - - Presign tokens should be created server-side. Never expose your API key in client-side code. - - ---- - -## Next Steps - -- [V1 Authoring](/docs/developers/embedding/authoring/v1) — Create and edit documents and templates using V1 components -- [V2 Authoring](/docs/developers/embedding/authoring/v2) — Create and edit envelopes using V2 components -- [CSS Variables](/docs/developers/embedding/css-variables) — Customize the appearance of embedded components -- [SDKs](/docs/developers/embedding/sdks) — Framework-specific SDK documentation diff --git a/apps/docs/content/docs/developers/embedding/authoring/meta.json b/apps/docs/content/docs/developers/embedding/authoring/meta.json deleted file mode 100644 index 45cb7a47b..000000000 --- a/apps/docs/content/docs/developers/embedding/authoring/meta.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "title": "Authoring", - "pages": ["v1", "v2"] -} diff --git a/apps/docs/content/docs/developers/embedding/authoring/v1.mdx b/apps/docs/content/docs/developers/embedding/authoring/v1.mdx deleted file mode 100644 index f10763fb3..000000000 --- a/apps/docs/content/docs/developers/embedding/authoring/v1.mdx +++ /dev/null @@ -1,300 +0,0 @@ ---- -title: V1 Authoring -description: Embed V1 document and template creation directly in your application. ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - -V1 authoring components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application. - - - Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is - also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). - Contact sales for access. - - -## Components - -The SDK provides four V1 authoring components: - -| Component | Purpose | -| ----------------------- | ----------------------- | -| `EmbedCreateDocumentV1` | Create new documents | -| `EmbedCreateTemplateV1` | Create new templates | -| `EmbedUpdateDocumentV1` | Edit existing documents | -| `EmbedUpdateTemplateV1` | Edit existing templates | - - ---- - -## Presign Tokens - -All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens. - - - - A presigned token is NOT an API token - - ---- - -## Creating Documents - -```jsx -import { EmbedCreateDocumentV1 } from '@documenso/embed-react'; - -const DocumentCreator = ({ presignToken }) => { - return ( -
- { - console.log('Document created:', data.documentId); - console.log('External ID:', data.externalId); - }} - /> -
- ); -}; -``` - ---- - -## Creating Templates - -```jsx -import { EmbedCreateTemplateV1 } from '@documenso/embed-react'; - -const TemplateCreator = ({ presignToken }) => { - return ( -
- { - console.log('Template created:', data.templateId); - }} - /> -
- ); -}; -``` - ---- - -## Editing Documents - -```jsx -import { EmbedUpdateDocumentV1 } from '@documenso/embed-react'; - -const DocumentEditor = ({ presignToken, documentId }) => { - return ( -
- { - console.log('Document updated:', data.documentId); - }} - /> -
- ); -}; -``` - ---- - -## Editing Templates - -```jsx -import { EmbedUpdateTemplateV1 } from '@documenso/embed-react'; - -const TemplateEditor = ({ presignToken, templateId }) => { - return ( -
- { - console.log('Template updated:', data.templateId); - }} - /> -
- ); -}; -``` - ---- - -## Props - -### All Authoring Components - -| Prop | Type | Required | Description | -| ------------------ | --------- | -------- | -------------------------------------------------------- | -| `presignToken` | `string` | Yes | Authentication token from your backend | -| `externalId` | `string` | No | Your reference ID to link with the document or template | -| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` | -| `css` | `string` | No | Custom CSS string (Platform Plan) | -| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) | -| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) | -| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) | -| `className` | `string` | No | CSS class for the iframe | -| `features` | `object` | No | Feature toggles for the authoring experience | - -### Update Components Only - -| Prop | Type | Required | Description | -| ---------------- | --------- | -------- | ---------------------------------------------------------- | -| `documentId` | `number` | Yes | The document ID to edit (for document update component) | -| `templateId` | `number` | Yes | The template ID to edit (for template update component) | -| `onlyEditFields` | `boolean` | No | Restrict editing to fields only, skipping recipient config | - ---- - -## Feature Toggles - -Customize what options are available in the authoring experience: - -```jsx - -``` - ---- - -## Event Callbacks - -All creation callbacks receive: - -| Field | Type | Description | -| ---------------------------- | -------- | --------------------------------------- | -| `documentId` or `templateId` | `number` | The ID of the created or updated item | -| `externalId` | `string` | Your external reference ID, if provided | - ---- - -## Field-Only Editing - -Restrict users to editing fields only, skipping recipient configuration: - -```jsx - { - console.log('Fields updated:', data.documentId); - }} -/> -``` - ---- - -## Complete Integration Example - -This example shows a full workflow where users create a document and then edit it: - -```tsx -import { useState } from 'react'; - -import { EmbedCreateDocumentV1, EmbedUpdateDocumentV1 } from '@documenso/embed-react'; - -const DocumentManager = ({ presignToken }) => { - const [documentId, setDocumentId] = useState(null); - const [mode, setMode] = useState('create'); - - if (mode === 'success') { - return ( -
-

Document updated successfully

- -
- ); - } - - if (mode === 'edit' && documentId) { - return ( -
- - { - console.log('Document updated:', data.documentId); - setMode('success'); - }} - /> -
- ); - } - - return ( -
- { - console.log('Document created:', data.documentId); - setDocumentId(data.documentId); - setMode('edit'); - }} - /> -
- ); -}; -``` - ---- - -## Additional Props - -Pass extra props to the iframe for testing experimental features: - -```jsx - -``` - - - Presign tokens expire after 1 hour by default. You can customize this duration based on your - security requirements. Generate fresh tokens for each session and avoid caching them on the client - side. - - ---- - -## See Also - -- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props -- [V2 Authoring](/docs/developers/embedding/authoring/v2) - V2 envelope authoring -- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance -- [Documents API](/docs/developers/api/documents) - Create documents via API -- [Templates API](/docs/developers/api/templates) - Create templates via API diff --git a/apps/docs/content/docs/developers/embedding/authoring/v2.mdx b/apps/docs/content/docs/developers/embedding/authoring/v2.mdx deleted file mode 100644 index 92b3b580d..000000000 --- a/apps/docs/content/docs/developers/embedding/authoring/v2.mdx +++ /dev/null @@ -1,344 +0,0 @@ ---- -title: V2 Authoring -description: Embed envelope creation and editing directly in your application. ---- - -import { Callout } from 'fumadocs-ui/components/callout'; - -V2 authoring components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API. - - - Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is - also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing). - Contact sales for access. - - -## Components - -The SDK provides two V2 authoring components: - -| Component | Purpose | -| ---------------------- | ------------------------ | -| `EmbedCreateEnvelope` | Create new envelopes | -| `EmbedUpdateEnvelope` | Edit existing envelopes | - ---- - -## Presign Tokens - -All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens. - - - A presigned token is NOT an API token - - ---- - -## Creating Envelopes - -Use `EmbedCreateEnvelope` to embed envelope creation. The `type` prop determines whether the envelope is created as a document or template. - -```jsx -import { EmbedCreateEnvelope } from '@documenso/embed-react'; - -const EnvelopeCreator = ({ presignToken }) => { - return ( -
- { - console.log('Envelope created:', data.envelopeId); - console.log('External ID:', data.externalId); - }} - /> -
- ); -}; -``` - -To create a template instead of a document, set `type` to `"TEMPLATE"`: - -```jsx - { - console.log('Template envelope created:', data.envelopeId); - }} -/> -``` - ---- - -## Editing Envelopes - -Use `EmbedUpdateEnvelope` to embed envelope editing: - -```jsx -import { EmbedUpdateEnvelope } from '@documenso/embed-react'; - -const EnvelopeEditor = ({ presignToken, envelopeId }) => { - return ( -
- { - console.log('Envelope updated:', data.envelopeId); - }} - /> -
- ); -}; -``` - ---- - -## Props - -### All V2 Authoring Components - -| Prop | Type | Required | Description | -| ---------------- | --------- | -------- | -------------------------------------------------------- | -| `presignToken` | `string` | Yes | Authentication token from your backend | -| `externalId` | `string` | No | Your reference ID to link with the envelope | -| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` | -| `css` | `string` | No | Custom CSS string (Platform Plan) | -| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) | -| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) | -| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) | -| `className` | `string` | No | CSS class for the iframe | -| `user` | `object` | No | Current user info. When provided, enables the "Add Myself" button in the recipients list. Object with optional `email` and `name` fields | -| `features` | `object` | No | Feature toggles for the authoring experience | - -### Create Component Only - -| Prop | Type | Required | Description | -| ---------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | `"DOCUMENT"` \| `"TEMPLATE"` | Yes | Whether to create a document or template envelope | -| `folderId` | `string` | No | The ID of the folder to create the envelope in. If not provided, the envelope is created in the root folder. The folder must match the envelope type and team. | - -### Update Component Only - -| Prop | Type | Required | Description | -| ------------ | -------- | -------- | ---------------------------- | -| `envelopeId` | `string` | Yes | The envelope ID to edit | - ---- - -## Feature Toggles - -V2 authoring provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the authoring experience — any omitted fields will use their defaults. - -```jsx - -``` - -### General - -Controls the overall authoring flow and UI: - -| Property | Type | Default | Description | -| ------------------------------- | --------- | ------- | ------------------------------------------------ | -| `allowConfigureEnvelopeTitle` | `boolean` | `true` | Allow editing the envelope title | -| `allowUploadAndRecipientStep` | `boolean` | `true` | Show the upload and recipient configuration step | -| `allowAddFieldsStep` | `boolean` | `true` | Show the add fields step | -| `allowPreviewStep` | `boolean` | `true` | Show the preview step | -| `minimizeLeftSidebar` | `boolean` | `true` | Minimize the left sidebar by default | - -### Settings - -Controls envelope configuration options. Set to `null` to hide envelope settings entirely. - -| Property | Type | Default | Description | -| ----------------------------------- | --------- | ------- | ----------------------------------------- | -| `allowConfigureSignatureTypes` | `boolean` | `true` | Allow configuring signature types | -| `allowConfigureLanguage` | `boolean` | `true` | Allow configuring the language | -| `allowConfigureDateFormat` | `boolean` | `true` | Allow configuring the date format | -| `allowConfigureTimezone` | `boolean` | `true` | Allow configuring the timezone | -| `allowConfigureRedirectUrl` | `boolean` | `true` | Allow configuring a redirect URL | -| `allowConfigureDistribution` | `boolean` | `true` | Allow configuring distribution settings | -| `allowConfigureExpirationPeriod` | `boolean` | `true` | Allow configuring the expiration period | -| `allowConfigureEmailSender` | `boolean` | `true` | Allow configuring the email sender | -| `allowConfigureEmailReplyTo` | `boolean` | `true` | Allow configuring the email reply-to | - -### Actions - -Controls available actions during authoring: - -| Property | Type | Default | Description | -| ------------------ | --------- | ------- | ------------------------ | -| `allowAttachments` | `boolean` | `true` | Allow adding attachments | - -### Envelope Items - -Controls how envelope items (individual files within the envelope) can be managed. Set to `null` to prevent any item modifications. - -| Property | Type | Default | Description | -| --------------------- | --------- | ------- | ------------------------------------ | -| `allowConfigureTitle` | `boolean` | `true` | Allow editing item titles | -| `allowConfigureOrder` | `boolean` | `true` | Allow reordering items | -| `allowUpload` | `boolean` | `true` | Allow uploading new items | -| `allowDelete` | `boolean` | `true` | Allow deleting items | -| `allowReplace` | `boolean` | `true` | Allow replacing an item's PDF | - -### Recipients - -Controls recipient configuration options. Set to `null` to prevent any recipient modifications. - -| Property | Type | Default | Description | -| --------------------------------- | --------- | ------- | ---------------------------------------- | -| `allowConfigureSigningOrder` | `boolean` | `true` | Allow configuring the signing order | -| `allowConfigureDictateNextSigner` | `boolean` | `true` | Allow configuring dictate next signer | -| `allowApproverRole` | `boolean` | `true` | Allow the approver recipient role | -| `allowViewerRole` | `boolean` | `true` | Allow the viewer recipient role | -| `allowCCerRole` | `boolean` | `true` | Allow the CC recipient role | -| `allowAssistantRole` | `boolean` | `true` | Allow the assistant recipient role | - -### Disabling Steps - -You can also disable entire steps of the authoring flow. This allows you to skip steps that are not relevant to your use case: - -```jsx - -``` - ---- - -## Event Callbacks - -### `onEnvelopeCreated` - -Fired when an envelope is successfully created: - -| Field | Type | Description | -| ------------ | ---------------- | --------------------------------------- | -| `envelopeId` | `string` | The ID of the created envelope | -| `externalId` | `string \| null` | Your external reference ID, if provided | - -### `onEnvelopeUpdated` - -Fired when an envelope is successfully updated: - -| Field | Type | Description | -| ------------ | ---------------- | --------------------------------------- | -| `envelopeId` | `string` | The ID of the updated envelope | -| `externalId` | `string \| null` | Your external reference ID, if provided | - ---- - -## Complete Integration Example - -This example shows a full workflow where users create an envelope and then edit it: - -```tsx -import { useState } from 'react'; - -import { EmbedCreateEnvelope, EmbedUpdateEnvelope } from '@documenso/embed-react'; - -const EnvelopeManager = ({ presignToken }) => { - const [envelopeId, setEnvelopeId] = useState(null); - const [mode, setMode] = useState('create'); - - if (mode === 'success') { - return ( -
-

Envelope updated successfully

- -
- ); - } - - if (mode === 'edit' && envelopeId) { - return ( -
- - { - console.log('Envelope updated:', data.envelopeId); - setMode('success'); - }} - /> -
- ); - } - - return ( -
- { - console.log('Envelope created:', data.envelopeId); - setEnvelopeId(data.envelopeId); - setMode('edit'); - }} - /> -
- ); -}; -``` - ---- - -## See Also - -- [Authoring Overview](/docs/developers/embedding/authoring) - V1 vs V2 comparison and presign tokens -- [V1 Authoring](/docs/developers/embedding/authoring/v1) - V1 document and template authoring -- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props -- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance diff --git a/apps/docs/content/docs/developers/examples/common-workflows.mdx b/apps/docs/content/docs/developers/examples/common-workflows.mdx index fe7887d5b..5bdf32cdf 100644 --- a/apps/docs/content/docs/developers/examples/common-workflows.mdx +++ b/apps/docs/content/docs/developers/examples/common-workflows.mdx @@ -8,6 +8,8 @@ import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + ## Workflow 1: Send a Document for Signature The most common workflow: upload a PDF, add recipients with signature fields, and send for signing. @@ -472,7 +474,7 @@ Send the same document to multiple recipients in parallel. Useful for policy ack distributeDocument: true - Process in batches with a short delay to respect rate limits (e.g. 100 requests/minute) + Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute) @@ -638,8 +640,8 @@ done - The API allows 100 requests per minute. For large batches, implement rate limiting with delays - between requests to avoid hitting limits. + The API allows 1000 requests per minute (your organisation may have its own lower limit). For large + batches, implement rate limiting with delays between requests to avoid hitting limits. --- @@ -998,9 +1000,12 @@ async function fetchWithRetry( // Retry on rate limit if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); - const delay = retryAfter ? parseInt(retryAfter) * 1000 : baseDelayMs * Math.pow(2, attempt); + // Honor Retry-After exactly; the cap only applies to the exponential fallback. + const delay = retryAfter + ? parseInt(retryAfter) * 1000 + : Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs); console.log(`Rate limited, waiting ${delay}ms...`); - await new Promise((resolve) => setTimeout(resolve, Math.min(delay, maxDelayMs))); + await new Promise((resolve) => setTimeout(resolve, delay)); continue; } diff --git a/apps/docs/content/docs/developers/examples/index.mdx b/apps/docs/content/docs/developers/examples/index.mdx index bdbcdb0b5..aab7191cc 100644 --- a/apps/docs/content/docs/developers/examples/index.mdx +++ b/apps/docs/content/docs/developers/examples/index.mdx @@ -3,6 +3,8 @@ title: Examples description: Common integration patterns and end-to-end workflows. --- + + + ## Prerequisites - A Documenso account (cloud or self-hosted) diff --git a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx index 5ae0a6c67..e87b85438 100644 --- a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx +++ b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx @@ -7,6 +7,8 @@ import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + + ## Prerequisites Before starting, you need: @@ -483,7 +485,7 @@ The API returns standard HTTP status codes and JSON error responses: ### Handling Rate Limits -The API allows 100 requests per minute per IP address. When rate limited, wait at least 60 seconds before retrying: +The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying: ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { diff --git a/apps/docs/content/docs/developers/getting-started/index.mdx b/apps/docs/content/docs/developers/getting-started/index.mdx index d2070f2b5..f38145d7e 100644 --- a/apps/docs/content/docs/developers/getting-started/index.mdx +++ b/apps/docs/content/docs/developers/getting-started/index.mdx @@ -3,6 +3,8 @@ title: Getting Started description: Get your API key and make your first API call. --- + + + ## Getting Started 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/developers/webhooks/events.mdx b/apps/docs/content/docs/developers/webhooks/events.mdx index 9f63f78ac..5bfaa7a42 100644 --- a/apps/docs/content/docs/developers/webhooks/events.mdx +++ b/apps/docs/content/docs/developers/webhooks/events.mdx @@ -33,13 +33,14 @@ All webhook events share a common structure: | Field | Type | Description | | ---------------- | --------- | ------------------------------------------------------ | -| `id` | number | Document or template ID | +| `id` | number | Legacy numeric v1 document or template ID | +| `envelopeId` | string | Canonical v2 identifier (`envelope_` + 16 characters) | | `externalId` | string? | External identifier for integration | | `userId` | number | Owner's user ID | | `authOptions` | object? | Document-level authentication options | | `formValues` | object? | PDF form values associated with the document | | `title` | string | Document or template title | -| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED` | +| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED`, `CANCELLED` | | `visibility` | string | Document visibility setting | | `createdAt` | datetime | Document creation timestamp | | `updatedAt` | datetime | Last modification timestamp | @@ -47,8 +48,8 @@ All webhook events share a common structure: | `deletedAt` | datetime? | Deletion timestamp | | `teamId` | number? | Team ID if document belongs to a team | | `templateId` | number? | Template ID if created from a template | -| `source` | string | Source: `DOCUMENT` or `TEMPLATE` | -| `documentMeta` | object | Document metadata (subject, message, signing options) | +| `source` | string | Source: `DOCUMENT`, `TEMPLATE`, or `TEMPLATE_DIRECT_LINK` | +| `documentMeta` | object? | Nullable document metadata (subject, message, signing options) | | `recipients` | array | List of recipient objects | | `Recipient` | array | List of recipient objects (legacy, same as recipients) | @@ -60,7 +61,6 @@ All webhook events share a common structure: | `subject` | string? | Email subject line | | `message` | string? | Email message body | | `timezone` | string | Timezone for date display | -| `password` | string? | Document access password (if set) | | `dateFormat` | string | Date format string | | `redirectUrl` | string? | URL to redirect after signing | | `signingOrder` | string | `PARALLEL` or `SEQUENTIAL` | @@ -77,8 +77,9 @@ All webhook events share a common structure: | Field | Type | Description | | ---------------------- | --------- | ------------------------------------------ | | `id` | number | Recipient ID | -| `documentId` | number? | Parent document ID | -| `templateId` | number? | Template ID if created from a template | +| `envelopeId` | string | Canonical parent envelope ID | +| `documentId` | number? | Legacy parent document ID; null for templates | +| `templateId` | number? | Legacy parent template ID; null for documents | | `email` | string | Recipient email address | | `name` | string | Recipient name | | `token` | string | Unique signing token | @@ -94,6 +95,8 @@ All webhook events share a common structure: | `sendStatus` | string | `NOT_SENT` or `SENT` | | `rejectionReason` | string? | Reason if recipient rejected | +Use `recipient.envelopeId` as the reliable parent link. The legacy `documentId` and `templateId` fields depend on the parent envelope type, so one of them is always null. + --- ## Document Lifecycle Events @@ -111,6 +114,7 @@ Triggered when a new document is created. "event": "DOCUMENT_CREATED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, @@ -129,9 +133,8 @@ Triggered when a new document is created. "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", - "timezone": "UTC", - "password": null, - "dateFormat": "MM/DD/YYYY", + "timezone": "Etc/UTC", + "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, @@ -145,6 +148,7 @@ Triggered when a new document is created. "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -166,6 +170,7 @@ Triggered when a new document is created. "Recipient": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -203,6 +208,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "event": "DOCUMENT_SENT", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, @@ -221,9 +227,8 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", - "timezone": "UTC", - "password": null, - "dateFormat": "MM/DD/YYYY", + "timezone": "Etc/UTC", + "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, @@ -237,6 +242,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -258,6 +264,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT" "Recipient": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -295,12 +302,14 @@ The recipient's `readStatus` changes to `OPENED`. "event": "DOCUMENT_OPENED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -328,6 +337,7 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. "event": "DOCUMENT_SIGNED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "status": "COMPLETED", "title": "contract.pdf", "source": "DOCUMENT", @@ -335,6 +345,7 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated. "recipients": [ { "id": 51, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -361,12 +372,14 @@ Triggered when an individual recipient completes their required action (signing, "event": "DOCUMENT_RECIPIENT_COMPLETED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -395,6 +408,7 @@ The document status changes to `COMPLETED` and `completedAt` is set. "event": "DOCUMENT_COMPLETED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 1, "authOptions": null, @@ -413,9 +427,8 @@ The document status changes to `COMPLETED` and `completedAt` is set. "id": "doc_meta_123", "subject": "Please sign this document", "message": "Hello, please review and sign this document.", - "timezone": "UTC", - "password": null, - "dateFormat": "MM/DD/YYYY", + "timezone": "Etc/UTC", + "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": null, "signingOrder": "PARALLEL", "allowDictateNextSigner": false, @@ -429,6 +442,7 @@ The document status changes to `COMPLETED` and `completedAt` is set. "recipients": [ { "id": 50, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "reviewer@example.com", @@ -451,6 +465,7 @@ The document status changes to `COMPLETED` and `completedAt` is set. }, { "id": 51, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -475,6 +490,7 @@ The document status changes to `COMPLETED` and `completedAt` is set. "Recipient": [ { "id": 50, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "reviewer@example.com", @@ -497,6 +513,7 @@ The document status changes to `COMPLETED` and `completedAt` is set. }, { "id": 51, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 10, "templateId": null, "email": "signer@example.com", @@ -537,12 +554,14 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont "event": "DOCUMENT_REJECTED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -561,7 +580,7 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont ### `document.cancelled` -Triggered when the document owner or a team member deletes a document. Draft and pending documents are hard-deleted, while completed documents are soft-deleted. +Triggered when a pending document is explicitly cancelled with `POST /envelope/cancel`, or when a document owner or team member deletes a document. Deleting a draft or pending document hard-deletes it, while deleting a completed document soft-deletes it. This event is **not** triggered when a recipient hides a document from their inbox. @@ -572,6 +591,7 @@ This event is **not** triggered when a recipient hides a document from their inb "event": "DOCUMENT_CANCELLED", "payload": { "id": 7, + "envelopeId": "envelope_abcdefhiklmnorst", "externalId": null, "userId": 3, "authOptions": null, @@ -591,7 +611,6 @@ This event is **not** triggered when a recipient hides a document from their inb "subject": "", "message": "", "timezone": "Etc/UTC", - "password": null, "dateFormat": "yyyy-MM-dd hh:mm a", "redirectUrl": "", "signingOrder": "PARALLEL", @@ -606,6 +625,7 @@ This event is **not** triggered when a recipient hides a document from their inb "recipients": [ { "id": 7, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 7, "templateId": null, "email": "signer@example.com", @@ -627,6 +647,7 @@ This event is **not** triggered when a recipient hides a document from their inb "Recipient": [ { "id": 7, + "envelopeId": "envelope_abcdefhiklmnorst", "documentId": 7, "templateId": null, "email": "signer@example.com", @@ -651,6 +672,45 @@ This event is **not** triggered when a recipient hides a document from their inb } ``` +### `recipient.expired` + +Triggered when a recipient's signing deadline passes on a pending document before they sign or reject it. + +**Event name:** `RECIPIENT_EXPIRED` + +The recipient's `expiresAt` contains the signing deadline, and `expirationNotifiedAt` is set when the expiration is processed. + +```json +{ + "event": "RECIPIENT_EXPIRED", + "payload": { + "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", + "status": "PENDING", + "title": "contract.pdf", + "source": "DOCUMENT", + "recipients": [ + { + "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", + "documentId": 10, + "templateId": null, + "email": "signer@example.com", + "name": "John Doe", + "role": "SIGNER", + "expiresAt": "2024-04-22T11:51:00.000Z", + "expirationNotifiedAt": "2024-04-22T11:52:00.000Z", + "readStatus": "OPENED", + "signingStatus": "NOT_SIGNED", + "sendStatus": "SENT" + } + ] + }, + "createdAt": "2024-04-22T11:52:00.000Z", + "webhookEndpoint": "https://your-endpoint.com/webhook" +} +``` + ### `document.reminder.sent` Triggered when a reminder email is sent to a recipient who has not yet completed their action. @@ -662,12 +722,14 @@ Triggered when a reminder email is sent to a recipient who has not yet completed "event": "DOCUMENT_REMINDER_SENT", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "status": "PENDING", "title": "contract.pdf", "source": "DOCUMENT", "recipients": [ { "id": 52, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "name": "John Doe", "role": "SIGNER", @@ -686,7 +748,7 @@ Triggered when a reminder email is sent to a recipient who has not yet completed ## Template Events -Template events track changes to reusable document templates. Template payloads use the same structure as document payloads, with `source` set to `TEMPLATE` and `templateId` populated. +Template events track changes to reusable document templates. Template payloads use the same structure as document payloads. For `TEMPLATE_CREATED`, `TEMPLATE_UPDATED`, and `TEMPLATE_DELETED` the template's own legacy numeric ID is in `id` and `templateId` is `null`. Only `TEMPLATE_USED` — whose payload describes the new document envelope created from the template — carries the originating template's legacy ID in `templateId`, with `source` set to `TEMPLATE`. ### `template.created` @@ -699,9 +761,10 @@ Triggered when a new template is created. "event": "TEMPLATE_CREATED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "title": "My Template", "status": "DRAFT", - "templateId": 10, + "templateId": null, "source": "TEMPLATE", "recipients": [] }, @@ -721,9 +784,10 @@ Triggered when a template's settings, recipients, or fields are modified. "event": "TEMPLATE_UPDATED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "title": "My Updated Template", "status": "DRAFT", - "templateId": 10, + "templateId": null, "source": "TEMPLATE", "recipients": [] }, @@ -743,9 +807,10 @@ Triggered when a template is deleted. "event": "TEMPLATE_DELETED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "title": "Deleted Template", "status": "DRAFT", - "templateId": 10, + "templateId": null, "source": "TEMPLATE", "recipients": [] }, @@ -765,6 +830,7 @@ Triggered when a document is created from a template. This event fires alongside "event": "TEMPLATE_USED", "payload": { "id": 10, + "envelopeId": "envelope_abcdefhiklmnorst", "title": "Document from Template", "status": "DRAFT", "templateId": 10, @@ -791,7 +857,8 @@ Triggered when a document is created from a template. This event fires alongside | `DOCUMENT_RECIPIENT_COMPLETED` | Recipient completes their action | Recipient `signingStatus: "SIGNED"`, `signedAt` set | | `DOCUMENT_COMPLETED` | All recipients complete actions | `status: "COMPLETED"`, `completedAt` set | | `DOCUMENT_REJECTED` | Recipient rejects document | Recipient `signingStatus: "REJECTED"`, `rejectionReason` set | -| `DOCUMENT_CANCELLED` | Owner or team member deletes document | Document cancelled or deleted | +| `DOCUMENT_CANCELLED` | Pending document explicitly cancelled, or document deleted | `status: "CANCELLED"` after explicit cancellation; deletion may remove or soft-delete the document | +| `RECIPIENT_EXPIRED` | Recipient signing deadline passes | Recipient `expiresAt` passed, `expirationNotifiedAt` set | | `DOCUMENT_REMINDER_SENT` | Reminder email sent to recipient | No status changes | ### Template Events @@ -821,7 +888,7 @@ When processing webhook events: **Process idempotently** — Webhooks may be retried, so handle duplicate events - **Respond quickly** — Return a 200 status code within 30 seconds + **Respond quickly** — Return a `2xx` status code within 10 seconds diff --git a/apps/docs/content/docs/developers/webhooks/index.mdx b/apps/docs/content/docs/developers/webhooks/index.mdx index 14bb89123..8c27eccbd 100644 --- a/apps/docs/content/docs/developers/webhooks/index.mdx +++ b/apps/docs/content/docs/developers/webhooks/index.mdx @@ -9,7 +9,7 @@ description: Receive real-time notifications for document and template events. 2. When an event occurs, Documenso sends an HTTP POST to your URL 3. Your application processes the event and responds with 200 OK -Documenso supports webhook events for the full document lifecycle (created, sent, opened, signed, completed, rejected, cancelled) as well as template events (created, updated, deleted, used). +Documenso supports webhook events for the full document lifecycle (created, sent, opened, signed, completed, rejected, cancelled), recipient-level events (recipient completed, reminder sent, recipient expired), and template events (created, updated, deleted, used). --- @@ -42,12 +42,14 @@ Documenso supports webhook events for the full document lifecycle (created, sent "event": "DOCUMENT_COMPLETED", "payload": { "id": 123, + "envelopeId": "envelope_abcdefhiklmnorst", "title": "Contract", "status": "COMPLETED", "completedAt": "2024-01-15T10:30:00.000Z", "recipients": [ { "id": 1, + "envelopeId": "envelope_abcdefhiklmnorst", "email": "signer@example.com", "signingStatus": "SIGNED" } @@ -58,6 +60,8 @@ Documenso supports webhook events for the full document lifecycle (created, sent } ``` +`payload.id` is the legacy numeric v1 ID. Use `payload.envelopeId` as the canonical v2 identifier. Each recipient repeats `envelopeId` as the reliable parent link because the legacy `documentId` and `templateId` fields depend on the parent envelope type, leaving one of them null. + --- ## See Also diff --git a/apps/docs/content/docs/developers/webhooks/setup.mdx b/apps/docs/content/docs/developers/webhooks/setup.mdx index 1725bec05..88fda57ea 100644 --- a/apps/docs/content/docs/developers/webhooks/setup.mdx +++ b/apps/docs/content/docs/developers/webhooks/setup.mdx @@ -148,7 +148,7 @@ func main() { - Always respond with a `200 OK` status within 30 seconds. Documenso will retry failed deliveries. + Always respond with a `2xx` status within 10 seconds. Documenso will retry failed deliveries according to the configured background-job provider. ## Configuring Webhooks in Documenso via the Dashboard @@ -184,7 +184,7 @@ Fill in the following fields: | Field | Description | | ----- | ----------- | -| **Webhook URL** | The HTTPS endpoint that will receive webhook events | +| **Webhook URL** | The HTTP or HTTPS endpoint that will receive webhook events | | **Events** | Select which events should trigger this webhook | | **Secret** (optional) | A secret key used to sign the payload for verification | @@ -202,12 +202,21 @@ Your webhook endpoint must meet these requirements: | Requirement | Details | | ----------- | ------- | -| **Protocol** | HTTPS required (HTTP not allowed in production) | -| **Response** | Must return `2xx` status code within 30 seconds | +| **Protocol** | HTTP and HTTPS are accepted; use HTTPS in production | +| **Response** | Must return a `2xx` status code within 10 seconds | | **Method** | Must accept HTTP POST requests | | **Content-Type** | Must accept `application/json` payloads | | **Availability** | Must be publicly accessible from the internet | + + Documenso performs a best-effort check that rejects webhook URLs which use or resolve to private + or loopback addresses. This is not a complete SSRF mitigation — it does not cover DNS rebinding + and fails open on DNS lookup errors or timeouts — so self-hosted deployments should still enforce + network-level egress rules. Self-hosters that need to deliver to a hostname resolving to a + private address can add that hostname to the comma-separated + `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` environment variable. + + For local development, use a tunneling service like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.me) to expose your local server. @@ -225,7 +234,8 @@ When creating a webhook, you can subscribe to one or more events: | `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action | | `DOCUMENT_COMPLETED` | All recipients have completed their actions | | `DOCUMENT_REJECTED` | A recipient rejects the document | -| `DOCUMENT_CANCELLED` | The document owner deletes the document | +| `DOCUMENT_CANCELLED` | A pending document is explicitly cancelled or a document owner deletes it | +| `RECIPIENT_EXPIRED` | A recipient's signing deadline passes before they sign or reject | | `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient | | `TEMPLATE_CREATED` | A new template is created | | `TEMPLATE_UPDATED` | A template is modified | @@ -294,6 +304,7 @@ Each webhook call shows the following details: - Timestamp - Response code - Request and response bodies +- Response headers Click any call to see full details including headers and response data. @@ -318,17 +329,17 @@ Documenso will attempt to deliver the same payload again ## Retry Policy -When a webhook delivery fails (non-2xx response or timeout), Documenso automatically retries with exponential backoff: +A delivery fails when the endpoint returns a non-`2xx` response, the 10-second timeout expires, or the request fails. Redirects are not followed, so `3xx` responses also fail. Network and SSRF-blocked requests are recorded with response code `0`. + +For self-hosted deployments, retries are handled by the background-job provider selected with `NEXT_PRIVATE_JOBS_PROVIDER`: -| Attempt | Delay | -| ------- | ----- | -| 1 | Immediate | -| 2 | 1 minute | -| 3 | 5 minutes | -| 4 | 30 minutes | -| 5 | 2 hours | +| Provider | Total attempts | Retry timing | +| -------- | -------------- | ------------ | +| Local (default) | 4 | Back-to-back, with no backoff | +| BullMQ | 3 | Exponential backoff starting at 1 second | +| Inngest | 5 | Inngest platform backoff | -After 5 failed attempts, the webhook is marked as failed and no further automatic retries occur. You can manually resend failed webhooks from the dashboard. +Only the individual delivery (`WebhookCall`) record is marked as failed. Documenso does not automatically disable the webhook or apply a circuit breaker, so future matching events continue to be delivered. After automatic attempts are exhausted, you can manually resend a failed delivery from the dashboard. If your endpoint consistently fails, consider reviewing your server logs and ensuring your endpoint meets all [URL requirements](#webhook-url-requirements). diff --git a/apps/docs/content/docs/developers/webhooks/verification.mdx b/apps/docs/content/docs/developers/webhooks/verification.mdx index a6cac916d..751d30d89 100644 --- a/apps/docs/content/docs/developers/webhooks/verification.mdx +++ b/apps/docs/content/docs/developers/webhooks/verification.mdx @@ -255,6 +255,7 @@ const validEvents = [ 'DOCUMENT_REJECTED', 'DOCUMENT_CANCELLED', 'DOCUMENT_REMINDER_SENT', + 'RECIPIENT_EXPIRED', 'TEMPLATE_CREATED', 'TEMPLATE_UPDATED', 'TEMPLATE_DELETED', diff --git a/apps/docs/content/docs/policies/enterprise-edition.mdx b/apps/docs/content/docs/policies/enterprise-edition.mdx index a611db5cd..f821e73ec 100644 --- a/apps/docs/content/docs/policies/enterprise-edition.mdx +++ b/apps/docs/content/docs/policies/enterprise-edition.mdx @@ -76,6 +76,8 @@ The Enterprise Edition is required when you: 4. Restart your Documenso instance 5. Verify the license is active in the **Admin Panel** under the **Stats** section + See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed. + @@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options. 1. Sign the Enterprise license agreement 2. Receive license key and access credentials 3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud - 4. Configure Enterprise features with support assistance + 4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance @@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options. ## Related +- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation - [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license - [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ - [Support](/docs/policies/support) - Support channels and response times diff --git a/apps/docs/content/docs/policies/fair-use.mdx b/apps/docs/content/docs/policies/fair-use.mdx index 0c4de348d..98d94dd5f 100644 --- a/apps/docs/content/docs/policies/fair-use.mdx +++ b/apps/docs/content/docs/policies/fair-use.mdx @@ -41,12 +41,17 @@ When a limit is reached, requests return a `429 Too Many Requests` response with | Action | Limit | Window | | --- | --- | --- | -| API requests (v1 and v2) | 100 requests | 1 minute | +| API requests (v1 and v2) | 1000 requests | 1 minute | | File uploads | 20 requests | 1 minute | | AI features | 3 requests | 1 minute | Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse. + + The API request limit above is the global per-IP ceiling. Individual organisations also have their + own rate limits, which may be configured below this value. + + Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact [sales](https://documen.so/sales) for details. diff --git a/apps/docs/content/docs/policies/meta.json b/apps/docs/content/docs/policies/meta.json index 14ba43d70..2251871a5 100644 --- a/apps/docs/content/docs/policies/meta.json +++ b/apps/docs/content/docs/policies/meta.json @@ -8,6 +8,7 @@ "privacy", "terms", "security", + "verify-email", "support" ] } diff --git a/apps/docs/content/docs/policies/verify-email.mdx b/apps/docs/content/docs/policies/verify-email.mdx new file mode 100644 index 000000000..f2d27277e --- /dev/null +++ b/apps/docs/content/docs/policies/verify-email.mdx @@ -0,0 +1,68 @@ +--- +title: Verifying Emails from Documenso +description: How to confirm that an email is genuinely from Documenso, and what to do if you receive a suspicious message. +--- + +import { Callout } from 'fumadocs-ui/components/callout'; + +## Check the Sender Domain + +All email sent by Documenso originates from one of the following domains. If you receive an email claiming to be from Documenso and the sender address does not end in one of these domains, treat it as suspicious. + +| Domain | Used for | +| ------------------------ | -------------------------------------------------------------- | +| `app.documenso.com` | Transactional email | +| `documensomail.com` | Transactional email | +| `documensoemail.com` | Transactional email | +| Custom domain | [Enterprise organisations](/docs/users/organisations/email-domains) using a custom email domain | + +Typical sender addresses include: + +- `noreply@app.documenso.com` +- `noreply@free.documensomail.com` +- `noreply@send.documensoemail.com` + + + A misspelling such as `documenso-email.com`, `documensoemaiI.com` (capital i instead of l), or any other variation is not a Documenso domain. + + +## Types of Email Documenso Sends + +Documenso sends email only for the following purposes: + +- **Account verification** — confirming your email address when you sign up or change it +- **Password reset** — a link to reset your password that you requested +- **Document invitations** — notifying you that a document has been shared with you to sign, approve, or view +- **Signing reminders** — follow-up reminders for pending document actions +- **Completed document notifications** — confirmation that all parties have signed a document +- **Team invitations** — inviting you to join an organisation or team + +## What Documenso Will Never Do + +- Ask for your password via email +- Send you an attachment and ask you to open it to verify your identity +- Ask you to confirm payment details or billing information over email +- Send unsolicited marketing emails if you have not opted in + +## How to Tell If an Email Is Legitimate + +1. **Check the sender address** — the domain must be `documenso.com` or `documensomail.com` +2. **Look at the link destination** — hover over any link before clicking; it should point to `app.documenso.com` +3. **Watch for urgency or threats** — legitimate Documenso emails do not threaten account suspension to pressure you into clicking a link immediately +4. **Verify the action yourself** — if in doubt, log in to [app.documenso.com](https://app.documenso.com) directly (not via the email link) and check whether the document or notification exists there + +## Report a Suspicious Email + +If you receive an email that appears to impersonate Documenso: + +1. Do not click any links or download any attachments +2. Forward the email as an attachment to **support@documenso.com** +3. Delete the email from your inbox + +You can also report phishing emails directly to your email provider using their built-in reporting tools. + +## Related + +- [Security Policy](/docs/policies/security) — Documenso's security practices and vulnerability disclosure process +- [Create an Account](/docs/users/getting-started/create-account) — What to expect during sign-up +- [Security Settings](/docs/users/settings/security) — Enable two-factor authentication and manage sessions 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 1d57b5062..bc4acd1a6 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 @@ -186,9 +201,9 @@ Documenso requires a certificate to digitally sign documents. ### Transport Selection -| Variable | Description | Default | -| -------------------------------- | ---------------------------------------- | ------- | -| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local` or `gcloud-hsm` | `local` | +| Variable | Description | Default | +| -------------------------------- | ------------------------------------------------- | ------- | +| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local`, `gcloud-hsm`, or `csc` | `local` | ### Local Signing @@ -210,11 +225,36 @@ Documenso requires a certificate to digitally sign documents. | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain | | `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval | +### Cloud Signature Consortium (CSC) + +Routes signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures (AES/QES). Instance-wide; set `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` to enable. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup walkthrough. + +CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Without a valid license, the instance will refuse to start in `csc` mode. + +| Variable | Description | Default | +| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- | +| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | | +| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | | +| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | | +| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller doesn't specify one. `AES` or `QES`. Explicit requests pass through. | `AES` | + +The OAuth callback URL registered with the CSC provider is fixed at `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` — register this exact URL with the TSP. + +#### Derived Public Variables + +The following client-visible variable is **derived automatically** from the private transport at server startup. Do not set it manually — any value set in the environment is overwritten on boot. + +| Variable | Derived from | Value | +| ------------------------------------- | -------------------------------------------------- | ------------------------------------------------- | +| `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` | `NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc'` | `'true'` when CSC mode is active, else `'false'` | + +The authoring UI uses this flag to gate features that AES/QES envelopes cannot support (parallel signing, assistant role, dictate next signer). Deriving it from the private transport prevents the client-side flag from drifting from the real server-side configuration. + ### Signature Options | Variable | Description | Default | | ------------------------------------------- | ----------------------------------------------------------- | ---------- | -| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures | | +| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures. Optional for `local` / `gcloud-hsm` (signatures omit the timestamp when unset). **Required** when `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` — the instance refuses to start without it. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes#timestamp-authority-resolution). | | | `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL | | `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` | @@ -232,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` | @@ -263,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 @@ -359,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal ## Enterprise Features -These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. +These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup. | Variable | Description | | ------------------------------------ | ------------------------------------------------ | -| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features | +| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it | | `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing | | `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret | | `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification | @@ -406,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" ``` --- @@ -416,4 +510,5 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" - [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery - [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage - [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing +- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Set per-organisation document, email, and API limits from the admin panel - [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues diff --git a/apps/docs/content/docs/self-hosting/configuration/index.mdx b/apps/docs/content/docs/self-hosting/configuration/index.mdx index 0e80e0efc..de5830a4d 100644 --- a/apps/docs/content/docs/self-hosting/configuration/index.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/index.mdx @@ -29,6 +29,11 @@ description: Configure your self-hosted Documenso instance with environment vari description="Digital signature certificate setup." href="/docs/self-hosting/configuration/signing-certificate" /> + ## Required Configuration diff --git a/apps/docs/content/docs/self-hosting/configuration/license.mdx b/apps/docs/content/docs/self-hosting/configuration/license.mdx new file mode 100644 index 000000000..eea50c154 --- /dev/null +++ b/apps/docs/content/docs/self-hosting/configuration/license.mdx @@ -0,0 +1,107 @@ +--- +title: Apply Your License Key +description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance. +--- + +import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; +import { Callout } from 'fumadocs-ui/components/callout'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance. + + + The license key applies to your **whole instance**, not an individual user account. There's one + key per deployment. + + +## Prerequisites + +- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an + Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com). + See [Enterprise Edition](/docs/policies/enterprise-edition) for details. +- A running self-hosted Documenso instance that you're able to restart + +## Step 1: Set the environment variable + +Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key. + + + + +Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`): + +```bash +NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here +``` + +Then apply it: + +```bash +docker compose up -d +``` + + + + +```bash +docker run -d \ + --name documenso \ + -e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \ + documenso/documenso:latest +``` + + + + +If you're running Documenso directly (not in a container), add the variable to your `.env` file: + +```bash +NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here +``` + + + + +## Step 2: Restart the instance + +The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts. + +```bash +# Docker Compose +docker compose restart documenso + +# Docker +docker restart documenso +``` + +On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out. + +## What the license enables + +A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature: + +- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup. +- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active. + +## Troubleshooting + + + + - Confirm the key is present in the environment the running process actually reads — `docker + exec` into the container and check `env | grep LICENSE` if unsure. + - Confirm the instance was fully restarted after the variable was set, not just reloaded. + - Re-copy the key to rule out truncation or accidental whitespace. + + + Instance-wide features (like CSC signing) also need their own configuration — an active license + alone isn't enough. Check that feature's guide to confirm the required settings are in place. + Per-organisation features additionally need to be provisioned for the organisation that's using + them. + + + +## See Also + +- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference +- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license +- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing diff --git a/apps/docs/content/docs/self-hosting/configuration/meta.json b/apps/docs/content/docs/self-hosting/configuration/meta.json index 32b92f853..6cc250f64 100644 --- a/apps/docs/content/docs/self-hosting/configuration/meta.json +++ b/apps/docs/content/docs/self-hosting/configuration/meta.json @@ -2,12 +2,14 @@ "title": "Configuration", "pages": [ "environment", + "license", "database", "email", "storage", "background-jobs", "signing-certificate", "telemetry", + "organisation-limits", "advanced" ] } diff --git a/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx new file mode 100644 index 000000000..c225975e5 --- /dev/null +++ b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx @@ -0,0 +1,111 @@ +--- +title: Organisation Limits +description: View and set per-organisation document, email, and API limits on a self-hosted Documenso instance using the admin panel's subscription claims. +--- + +import { Callout } from 'fumadocs-ui/components/callout'; + +Per-organisation limits — document, email, and API usage, plus feature toggles and team/member caps — are controlled by **subscription claims**. You configure them in the admin panel, not through environment variables. + +There are three distinct kinds of limit: + +| Limit | Caps | Admin-settable | +| ---------------------- | ------------------------------------------------- | ----------------------- | +| Resource quota | Documents, emails, and API requests **per month** | Yes — per claim and org | +| Resource rate limit | The same resources over a short window (e.g. `1h`) | Yes — per claim and org | +| Global HTTP rate limit | API requests per IP (1000/min, hardcoded) | No — see [Limitations](#limitations) | + +## Prerequisites + +- A running self-hosted Documenso instance. +- An account with the **`ADMIN`** role — an account-level role, separate from organisation and team roles. New accounts are created with the `USER` role only. Grant the first admin by adding `ADMIN` to that user's `roles` directly in the database; after that, an existing admin can grant the role to others under **Admin Panel > Users > _(user)_ > Roles > Update user**. + +Open the admin panel at `/admin`. The sidebar sections used below are **Claims**, **Organisations**, and **Organisation Stats**. + +## Viewing usage + +**One organisation:** open **Admin Panel > Organisations** and select it. The **Organisation usage** section shows the current period's document, email, and API usage against its quotas. + +**All organisations:** open **Admin Panel > Organisation Stats** to sort and filter monthly usage. Filter by **claim** and by **period** (a UTC calendar month, shown as `YYYY-MM`), and switch between **Show usage**, **Show usage with quotas**, and **Show daily averages**. + + + Usage counts **attempts**, not only successful actions. A request that exceeds a quota is still counted before it is rejected, so displayed usage can read higher than the number of actions that succeeded. + + +## Subscription claims + +A subscription claim is a named bundle of limits and feature flags (for example `Free`, `Individual`, `Teams`, `Platform`, or `Enterprise`). Claims are **templates**: when an organisation is created it receives a private copy of its claim and reads from that copy afterwards. Editing a claim template therefore affects organisations created later, not existing ones — to change an existing organisation, [edit it directly](#change-limits-for-one-organisation). + +### Claim fields + +Under **Admin Panel > Claims** (`/admin/claims`), each claim has: + +| Field | Controls | +| ----------------------- | --------------------------------------------------------------------------------- | +| **Name** | The claim's display name. | +| **Team Count** | Teams allowed. `0` = unlimited. | +| **Member Count** | Members allowed. `0` = unlimited. | +| **Envelope Item Count** | Uploaded files allowed per envelope. Minimum `1`. | +| **Recipient Count** | Recipients allowed per document. `0` = unlimited. | +| **Feature Flags** | Feature toggles (see [Feature flags](#feature-flags)). | +| **Limits** | Monthly quota and rate-limit windows for Documents, Emails, and API. | +| **Email transport** | Transport the claim uses. *Default (system mailer)* uses the instance default. | + +### Quotas and rate limits + +The **Limits** section has a column for **Documents**, **Emails**, and **API**, each with two controls: + +- **Monthly quota** — how many of that resource are allowed per calendar month. An **empty** field is unlimited; **`0`** blocks the resource entirely. +- **Rate limit windows** — optional short-window caps, each a duration and a maximum. A window is a number and a unit (`s`, `m`, `h`, `d`), such as `5m`, `1h`, or `24h`, and must be unique within the resource. + + + Quotas and counts use opposite conventions for "unlimited": an **empty** quota is unlimited (and `0` blocks the resource), whereas `0` in the **Team**, **Member**, and **Recipient Count** fields means unlimited. + + +### Feature flags + +The **Feature Flags** section toggles capabilities such as Unlimited documents, Branding, Hide Documenso branding, Email domains, Embed authoring, Embed signing, White label for embed authoring/signing, 21 CFR, HIPAA, Authentication portal, Allow Legacy Envelopes, Signing reminders, QES signing, and Disable emails. + +Some flags are Enterprise features. If your license does not include one, it is marked and cannot be enabled (you can still turn it off). See [Enterprise Edition](/docs/policies/enterprise-edition). + +### Create or edit a claim template + +1. Go to **Admin Panel > Claims**. +2. Select **New claim**, or select an existing claim to edit it. +3. Set the counts, feature flags, and the **Limits** section. +4. Save. Changes apply to organisations created afterwards, not existing ones. + +### Change limits for one organisation + +To change limits for an existing organisation, edit it directly rather than its claim template. + +1. Go to **Admin Panel > Organisations** and open the organisation. +2. Adjust its quota, rate-limit, feature-flag, or email-transport fields. +3. Save. Changes take effect immediately. + +The organisation also shows the **Inherited subscription claim** it was created from. + +## Usage reset + +Monthly quota usage is keyed to the **UTC calendar month**. There is no scheduled reset job — when the month rolls over, the new period's counter starts at `0`. + +## Limitations + +The **global HTTP rate limit is not configurable.** Documenso enforces a hardcoded **1000 requests per minute per IP address** on its API endpoint groups (`/api/v1`, `/api/v2`, and the tRPC API are limited separately), returning `429 Too Many Requests`. It is a per-IP safeguard applied at the HTTP layer — not per-organisation, not stored on any claim, and not adjustable from the admin panel. See [Rate Limits](/docs/developers/api/rate-limits). + +## Troubleshooting + +| Symptom | Cause and fix | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | +| An organisation hit its limit unexpectedly | Usage counts rejected over-quota attempts. Compare usage against the quota under **Organisation Stats > Show usage with quotas**. | +| A resource is blocked entirely, not just capped | The **Monthly quota** is `0`, which blocks the resource. Leave it empty for unlimited. | +| Emails are not sending for an organisation | Check whether the **Disable emails** flag is enabled on the organisation's claim — it blocks all emails regardless of quota. | +| A claim template edit had no effect | Template edits are not retroactive. Edit the organisation directly under **Admin Panel > Organisations**. | + +--- + +## See Also + +- [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options +- [Rate Limits](/docs/developers/api/rate-limits) - The global HTTP API rate limit (separate from claims) +- [Enterprise Edition](/docs/policies/enterprise-edition) - Features unlocked by license flags diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx new file mode 100644 index 000000000..4998baaf7 --- /dev/null +++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx @@ -0,0 +1,213 @@ +--- +title: CSC (AES / QES) +description: Configure Cloud Signature Consortium signing for Advanced and Qualified Electronic Signatures via a third-party Trust Service Provider. +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { Step, Steps } from 'fumadocs-ui/components/steps'; + +The `csc` signing transport routes signatures through a third-party Trust Service Provider (TSP) using the [Cloud Signature Consortium API v1.0.4.0](https://cloudsignatureconsortium.org/). Each recipient authenticates directly with the TSP (Strong Customer Authentication) and the TSP returns a per-recipient signature bound to the document hash. Documenso assembles the resulting PAdES signature inside the PDF. + +This transport enables **Advanced Electronic Signatures (AES)** and **Qualified Electronic Signatures (QES)** under eIDAS. See [Signature Levels](/docs/compliance/signature-levels) for the legal framework. + + + CSC mode is **instance-wide**: one CSC provider per Documenso install. All envelopes created + while the instance runs in `csc` mode use AES or QES. Switching `NEXT_PRIVATE_SIGNING_TRANSPORT` + is a one-way operational migration — see [Switching Transports](#switching-transports). + + + + CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. The + instance refuses to start in `csc` mode without it. + + +## Prerequisites + +{/* prettier-ignore */} + + + +### A TSP account + +Establish a relationship with a CSC-compatible Trust Service Provider. The TSP issues qualified or advanced certificates to your signers, holds the private keys in its HSM, and exposes a CSC v1.0.4.0-compliant API. + + + + +### OAuth client credentials + +Register Documenso as an OAuth client with the TSP. You will receive a client ID and client secret, and must supply Documenso's callback URL when registering: + +``` +${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback +``` + +The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` and the route mount path. There is no env var to override it; ensuring the registered URL matches your instance's webapp URL exactly is the operator's responsibility. + + + + +### Enterprise Edition license + +CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one. + + + + +### S3 storage (strongly recommended) + +CSC produces multiple `DocumentData` rows per envelope item (one per recipient signature, plus the materialised and source rows). Database-backed storage base64-inflates each row by ~33% and is impractical at meaningful PDF sizes. Configure [S3 storage](/docs/self-hosting/configuration/storage) before enabling CSC. + + + + +## Environment Variables + +| Variable | Description | Default | +| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `csc` | | +| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | | +| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | | +| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | | +| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller does not specify one. `AES` or `QES`. Explicit requests always pass through. | `AES` | +| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | **Required.** Comma-separated RFC 3161 TSA URLs. Always used for B-LTA archival timestamps at seal time, and also serves as the B-T sign-time fallback when the TSP does not expose `signatures/timestamp`. The instance refuses to start in CSC mode without it. See [Timestamp Authority Resolution](#timestamp-authority-resolution). | | + + + `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` is set automatically from + `NEXT_PRIVATE_SIGNING_TRANSPORT` at server startup. Do not set it manually — see + [Environment Variables](/docs/self-hosting/configuration/environment#derived-public-variables). + + +## Configuration Example + +```bash +NEXT_PRIVATE_SIGNING_TRANSPORT=csc +NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=https://api.example-tsp.com/csc/v1 +NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=documenso-prod +NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=... +NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=QES +NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.example.com +``` + +Register `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` (e.g. `https://sign.example.com/api/csc/oauth/callback`) as the OAuth callback URL with the TSP. + +## Default Signature Level + +`NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` selects the legal tier applied to envelopes that do not specify one explicitly. It is a default, not a capability gate: callers may still create AES or QES envelopes explicitly regardless of this setting. + +| Configured value | Caller passes nothing | Caller passes `AES` | Caller passes `QES` | +| ---------------- | --------------------- | ------------------- | ------------------- | +| `AES` (default) | Envelope is `AES` | Envelope is `AES` | Envelope is `QES` | +| `QES` | Envelope is `QES` | Envelope is `AES` | Envelope is `QES` | + +Any value other than `AES` or `QES` causes the instance to refuse to start. This prevents silent qualified-to-advanced downgrades from a typo. + +## Timestamp Authority Resolution + +AES/QES envelopes use TSA-attested timestamps in two distinct phases. Resolution differs per phase. + +### Sign time — PAdES B-T per recipient + +Each recipient's CMS embeds a signature timestamp (CMS unsigned attribute) so proven time is bound to the recipient's signature itself. Resolution order: + +1. If the TSP advertises `signatures/timestamp` in its `info` response (CSC §11.10), the TSP endpoint is used. The call is authorised with **this recipient's** service-scope bearer token — the same one authorising the `signatures/signHash` call alongside it. +2. Otherwise, the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is used (RFC 3161 over HTTP). + +Selection is made at boot from the discovered transport, not at runtime; there is no try-then-fall-through. If the chosen source fails, the recipient's sign attempt fails. + +### Seal time — PAdES B-LTA archival + +The seal-document job emits a single archival `/DocTimeStamp` over the fully-signed envelope (plus DSS for the existing signatures and the timestamp's own chain). This phase is **env-only**: the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is always used. + +The archival anchor is the operator's long-term trust anchor and SHOULD point at a dedicated qualified archival TSA (e.g. DigiCert) independent of the per-recipient TSP. We deliberately do not fall back to the TSP at seal time: archive longevity should not be coupled to a TSP that may rotate or revoke, and the seal-document job has no recipient context to carry a service-scope bearer. + +### Boot-time guard + +The instance refuses to start in CSC mode unless `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is set (`CSC_PROVIDER_NO_TSA` at transport construction). The env var is required unconditionally — even when the TSP advertises its own `signatures/timestamp`, seal-time B-LTA archival uses the env TSA. Catching this at boot prevents the failure mode where an envelope signs successfully at B-T and then hangs in `WAITING_FOR_SIGNATURE_COMPLETION` when the seal job throws. + +## Switching Transports + +`NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration. Existing envelopes route per the `signatureLevel` column they were created with — the runtime branching looks at the envelope, not the env var. After a switch: + +- Envelopes already at `SES` continue to use the new transport for sealing, but the new transport's signer must produce SES-compatible signatures (only `local` and `gcloud-hsm` qualify). +- Envelopes already at `AES` / `QES` will fail at sign or seal time if the new transport is not `csc`. + +Plan migrations during a quiet window with no in-flight envelopes. + +## Behavioural Notes + +CSC mode changes a number of envelope-authoring behaviours that operators should communicate to users. + +### Mutation lock at distribution + +For AES/QES envelopes, all authoring routes refuse mutations once the envelope leaves DRAFT. This locks the PDF before any recipient begins Strong Customer Authentication, closing the PDF-swap window that would otherwise allow an owner to replace the PDF between view and sign and break the legal "what you see is what you sign" guarantee. + +In practice: edit envelope, recipients, fields, and items freely while DRAFT; once sent, no changes are accepted (including from the API). + +### Sequential signing only + +Parallel signing produces conflicting incremental updates over the same base PDF, breaking the per-recipient `/ByteRange` invariant. The signing order is forced to `SEQUENTIAL` on AES/QES envelopes — at the schema layer, at send time, and in the UI (the parallel-signing toggle is hidden). + +### Assistant role and Dictate Next Signer disabled + +Both features modify the recipient set after the envelope is sent, which is incompatible with the AES/QES mutation lock. They are hidden in the UI and rejected at the server schema layer. + +### Sidecar PDFs at download + +The signed PDF must remain byte-identical to what each recipient's TSP signature authorised — Documenso cannot decorate it after signing. Audit logs and the Certificate of Completion are generated on demand and delivered as separate PDFs: + +- `GET /sign/{token}/download` returns the signed PDF only (or a ZIP for multi-item envelopes). +- `GET /sign/{token}/download?version=bundle` returns a ZIP containing the signed PDFs, audit log PDF, and Certificate of Completion. +- The completion email attaches all three. + +## Recipient Flow + +For context when supporting end users, here is what a recipient experiences on an AES/QES envelope: + +1. Opens the email link, lands on the signing page. +2. Documenso redirects to the TSP for Strong Customer Authentication (first visit only; cached for the session lifetime). +3. Fills fields as normal. +4. Clicks Sign → redirected to the TSP for a second authentication round (issues a per-document Signature Activation Data token). +5. Returns to Documenso; the signing call completes within ~15 seconds. +6. Sees the standard completion screen. + +If the TSP returns no eligible credentials for the recipient (e.g. they have not enrolled), they see a blocking page directing them to enrol with the TSP and retry. + +## Error Codes + +CSC-specific error codes surfaced through the standard error channels: + +| Code | Meaning | Recovery | +| -------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- | +| `CSC_UNLICENSED` | License flag absent at transport-create | Operator: enable Enterprise Edition, restart | +| `CSC_PROVIDER_INFO_FAILED` | `info` discovery failed at startup | Operator: check TSP availability and `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | +| `CSC_PROVIDER_NO_TSA` | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is unset | Operator: configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | +| `CSC_CREDENTIAL_LIST_EMPTY`| TSP returned no credentials for the user | Recipient: enrol with the TSP | +| `CSC_CERT_INVALID` | Certificate refused at credential validation | Recipient: contact the TSP | +| `CSC_ALGORITHM_REFUSED` | Signature algorithm fails policy | Operator/recipient: TSP does not meet policy (see below) | +| `CSC_SAD_EXPIRED_PRE_SIGN` | Signature Activation Data expired before signing | Recipient: retry from Sign | +| `CSC_TSP_TIMEOUT` | 15-second synchronous timeout reached | Recipient: retry (idempotent — the TSP enforces single-use SAD binding) | +| `CSC_EMBED_FAILED` | Sign-time digest diverged from prep capture | Recipient: retry from Sign | +| `CSC_BASE_DOCUMENT_MUTATED`| Document data changed between prep and sign | Operator: investigate (structural guard violation) | +| `CSC_INSTANCE_MODE_MISMATCH`| Envelope created with wrong level for transport | Caller: use a level matching the instance transport | +| `CSC_REQUEST_FAILED` | TSP HTTP transport failure — network error, non-2xx, or malformed response | Operator: check TSP availability; carries the TSP HTTP status and error in the message | + +## Algorithm Policy + +Documenso refuses TSP credentials that do not meet the following minimums, at the OAuth callback boundary and again at sign time: + +| Class | Allowed | Refused | +| ----- | ---------------------------------- | ------------------------------------------------------ | +| RSA | `key.len >= 2048` | Missing `key.len`, `key.len < 2048` | +| ECDSA | P-256, P-384, P-521 | Missing `key.curve`, P-192, P-224, other curves | +| Hash | SHA-256, SHA-384, SHA-512 | SHA-1, MD5 | +| Other | — | DSA | + +This is the union of CSC v1.0.4.0 §11.5 requirements and current cryptographic guidance. + +## Related + +- [Signature Levels](/docs/compliance/signature-levels) — AES / QES legal framework +- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) — overview of all signing transports +- [Environment Variables](/docs/self-hosting/configuration/environment) — full env reference +- [Enterprise Edition](/docs/policies/enterprise-edition) — license requirements diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx index 89a12a327..a2d5a0a7c 100644 --- a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx +++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx @@ -24,6 +24,11 @@ Self-hosted Documenso instances require a signing certificate. You can generate description="Hardware-based key protection with Google Cloud KMS." href="/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm" /> + + A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations. @@ -79,6 +84,18 @@ For organisations requiring hardware-based key protection, Documenso supports Go See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions. + + + +For Advanced and Qualified Electronic Signatures under eIDAS, Documenso integrates with third-party Trust Service Providers via the Cloud Signature Consortium API. Each recipient authenticates directly with the TSP, which holds the private key and issues the signature. + +- Per-recipient identity verification by an accredited TSP +- Legally equivalent to a handwritten signature within the EU (QES) +- Requires an [Enterprise Edition](/docs/policies/enterprise-edition) license +- Instance-wide setting; one CSC provider per Documenso install + +See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for setup instructions. + diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json index d37038af3..b52f83f9e 100644 --- a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json +++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json @@ -1,4 +1,4 @@ { "title": "Signing Certificate", - "pages": ["...index", "local", "google-cloud-hsm", "timestamp-server", "troubleshooting"] + "pages": ["...index", "local", "google-cloud-hsm", "csc-qes", "timestamp-server", "troubleshooting"] } 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..a5ac58807 100644 --- a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx @@ -81,7 +81,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?err} - POSTGRES_DB=${POSTGRES_DB:?err} healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}'] + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] interval: 10s timeout: 5s retries: 5 @@ -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/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/deployment/railway.mdx b/apps/docs/content/docs/self-hosting/deployment/railway.mdx index 93c861678..501edca1c 100644 --- a/apps/docs/content/docs/self-hosting/deployment/railway.mdx +++ b/apps/docs/content/docs/self-hosting/deployment/railway.mdx @@ -24,7 +24,7 @@ Before deploying, you need: The fastest way to deploy Documenso on Railway is using the official template: -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/bG6D4p) +[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic) This template automatically provisions: @@ -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/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..99e4ecb01 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 @@ -133,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key. -See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. +See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license). --- 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/docs/content/docs/users/getting-started/create-account.mdx b/apps/docs/content/docs/users/getting-started/create-account.mdx index 17030e730..8047f48ea 100644 --- a/apps/docs/content/docs/users/getting-started/create-account.mdx +++ b/apps/docs/content/docs/users/getting-started/create-account.mdx @@ -39,7 +39,11 @@ Navigate to [documen.so/free](https://documen.so/free) to create a free account. Provide your name, email address, and create a password. Alternatively, sign up with Google for faster access. -{/* TODO: Add screenshot of registration form */} +Documenso registration form with name, email, and password fields diff --git a/apps/docs/content/docs/users/settings/delete-account.mdx b/apps/docs/content/docs/users/settings/delete-account.mdx index 569dc1bc2..77640db70 100644 --- a/apps/docs/content/docs/users/settings/delete-account.mdx +++ b/apps/docs/content/docs/users/settings/delete-account.mdx @@ -7,14 +7,14 @@ import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; - Account deletion is permanent and irreversible. All documents, signatures, templates, and account - data will be permanently removed. Any active subscription will be cancelled. + Account deletion is permanent and irreversible. Your account, signatures, and personal data will be + permanently removed, and any active subscription will be cancelled. How your organisations and + documents are handled is explained below. ## Before Deleting - Download any documents you need to keep -- Cancel any active subscriptions - Disable two-factor authentication (required before deletion) ## Delete Your Account @@ -36,6 +36,31 @@ import { Step, Steps } from 'fumadocs-ui/components/steps'; If you have two-factor authentication enabled, you must disable it before deleting your account. +## What Happens to Your Organisations + +When you delete your account, the organisations you **own** are permanently deleted along with all of +their teams. If an owned organisation has an active subscription, it is scheduled for cancellation at +the end of the current billing period. + +Organisations that you are only a **member** of are not deleted. You are simply removed from them, and +the organisation continues to operate as normal. + +## What Happens to Your Documents + +The way your documents and templates are handled depends on whether you owned the organisation they +belong to: + +- **Organisations you owned** — Completed and in-progress documents are retained in an anonymized form + (reassigned to an internal system account) so the other parties keep their records. Draft documents + and templates are permanently removed. +- **Organisations you were a member of** — Your documents and templates are transferred to the + organisation owner, so they remain accessible to the organisation after you leave. + + + Documents that are retained in anonymized form are no longer associated with your account and cannot + be recovered or accessed by you after deletion. Download anything you need to keep beforehand. + + --- ## See Also diff --git a/apps/docs/package.json b/apps/docs/package.json index 76aecb716..9539148d0 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,13 +10,12 @@ "postinstall": "fumadocs-mdx" }, "dependencies": { - "@radix-ui/react-tabs": "^1.1.13", - "fumadocs-core": "16.5.0", - "fumadocs-mdx": "14.2.6", - "fumadocs-ui": "16.5.0", + "fumadocs-core": "16.14.3", + "fumadocs-mdx": "15.2.3", + "fumadocs-ui": "16.14.3", "lucide-react": "^0.563.0", "mermaid": "^11.12.2", - "next": "16.2.6", + "next": "16.3.0", "next-plausible": "^3.12.5", "next-themes": "^0.4.6", "react": "^19.2.4", @@ -29,7 +28,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/docs/public/get-started-images/documenso-registration-form.webp b/apps/docs/public/get-started-images/documenso-registration-form.webp new file mode 100644 index 0000000000000000000000000000000000000000..5b414adc5b0b467759e64e4733cea3128b6ecc25 GIT binary patch literal 36930 zcmaI7V|b+9)-4>{wrzEcj%}l3+fFLBZ5thQ)Uln8ZQEvj&$IDxf zkfv6jr!T#W!x#D2Qz`aLw&puAF3qa>ho{UD<7!&26WC}1}yu40M&umU;9^*`yLAp*VGk&GvXt_4WQ!__$msN z2ZnztUIWL8Pl*oz55Ty;J_|enf1`e$e%qP>TmsI0VgUL;X<*$3{HxFl@h4Hoc01sy zTLI7xnE9Lp=HF4js{>m+?RaGe71l_>=w}`F#2*{#~&B zi~g=>UZ~!`3!n+?`I^|(eBK_87uL`J06bsc8Z--a=TiXC-hMv{T?*|2M*$|EP4Bbs z@lV8K+ZXxm`g0%jpYiX@7hKQr8_f00!#!~T<*#3#`ycsF4xfO_&p}|^N7>i%K4B3s z1xWuk@+JBVJi%TBrUJdbR=-p~)1MtSnN5f{g!=tA03pD`JBwqXg`O^;-DlO;I`I4k z{H5oP`PyI|Xb0#3yaPR+9A4vZ0lEIE+s;7JulrBreUBCFJBL%^lb+vzlD`(<0t9|3 z4hd}nwg5grdZ5K8HW2s%#1LZpTK_u!V17gX*gg^Z0<3*Peu;nFe~dg$-{s!}2LNBd zfoD75#QO2&^;eI}^z!z{cB>Gu{}#}ghK|=@-9^boPBzKD?5|TNEk8Q?3*m7)t$I$? znT?^}$t|Wb9c@ofHqwcSa;_G5 zp{e^%%B0^$&Af=G`aRaO?8b4^=!q9Lirg{T%=zG~VJyQq1Nb(?$S1L#pgu2F?(MDi z+-`p^m|y^Eh^UG+o5=%ua>-LQP1?LA;DwWYMK>*l|n{jAB?@-mA4clwwxEhY$i1(7?V+!Gdyv zcR!AX$}Wk49oRZ86w?P9*(4q;3Uj^_s|?rm9LJ`J?>sZ~*8t>_BiWiI2ITUky>xsGX4SIWtRW`>(}jJ=xB^0IxcuEvZkB>7wI>s zQ1wFo67O5-1xGPfw(eaP=BG4BX7YWayA)a}nVk0UURDp^{p!Qp z{MoO*OSEBH$j|(ap|dw*QMhiSeQN@Rm~^5xO`jG zSCC4B|5c%%T}5BovdUs$An0Oo(XQLWJ;rf#2SMP8?kndZnPHW9ubyt=ZV1o#!x$Q~ z*8DMlAt~4#hnAy*NYRZMY>03ErhYcM-&W+AK5?%$(D}Jw7>a+Jx6Z!RW5=>H`+I|S zGRHZDeotu$eNoQ7)6_;}BSYPDtKY^qI0c2_HSM2)zOro@G|xkyBIE9sw9VD%5)8~L zfnf$4VoC{QuwKQP71CrXXhzJ={f0Ca>G?@Him4Blk)Wj{Opp8Xyd0P1+N1)mbkAD>wU zZ>KE~|E41e1w=qLXwZ)U&V9_D9H%X#cX+ad&}+O1+?anh)?afP2k_7TK(QI&lxlF3fAz?}(*3(#B6VAwZ8LIyKP_X@@smUw#Bg2yq*L=0#_Ilr z_tYSwV?|AeuqHA8kEL=hY2<5;asWms+l(6@!dFvkTYY}evie0r6aC(aG76hX8&@xpZaTPp;hzFm-t_~#=7Av z0dacloK%YYVVYkC{MYtMB|}hcZoM6&FDHl!J;DD+4*#i)M`Z{cJ?>h+FOgSZ97j~0 zS^O$nMCpUckV997HxbhY(x_#FQosKvfW`rP`cMCe|39g84VhONBmJL2!+e{U$Hpyw zM)KN%{)Zaxtf|gFz|sqSa-)9D7xGT1PTD@Fu(thIlpkK5-(r&z?U$}IS&czni1Yt@ z_Fx?w9+%kOA1&av34uE+|J1y{N%oig38gn>g>|X7QMWv?;@*=8xNWSuyqSY0mM*%- z9*;$!3jlvEnnyM_yKI`NS^W$7e-)wpN;sU}hoZ%=c}ON6hHQBs@!cjWl?ZN^|9;fW zrk>35zgR_Gm8f6_@Ct~R8mR%H|>{|7g3=9HmQQ* zmx5#%EWeReOP;xZLPbTC+=3{EFpSfFyMK~X5pcc_5x$Z20XCSCjAkq+9T^m>_MPZ2 zcZ=_Zk-#B@zi^)HeZ#}0C`WNDXV9LbZ{~)AsHhGhUAn93ibWB}v{b{pAckbr;)}_{OUaP-J zN@f)I_BU(I`<&dYr}Kpf=JoPGZvVkTbfi`(pkBh?Er36_?;Tz@KVL2OZ`)R`BNK_< z|HqP9sVM%n`pzH^J%ImmW^p>%g z5YMpq`O&POl%1HM@@yh-!kih8>rLg7u}!W9D-QrN*GR;*n%ouh7f zSDH-LG?L_1A9_6fNndQ;y14qB4+T8z1Rl0=_wE;2J2)u@;s4sapg!9SsU-H;{BBoV z|52V$6?O#dJ`%cm+;6iieuTXX>3X9T)J}Kdw6FhX zOMI7t{4fi!HAZC~!~*@r1?LJ~xb%w|<8!-;yE*lm#v467@by0>hhX4^&gYdYNwefl z>EdrJZh{#7S+-l+DwTI3bUus-hnM~)$Uin8{}$G_qz{eaA(o9vcC>I?-RXTWWd_A@Lj5%xp~+mBB?Aq91!rMcub8g$-U--~MOE1|vE)^>0!8x2UXZ{TqOPS9oxk z@D=8S>TjX`SB3tSRSSe+I0jtpceMjAVQqBSXjq%k)!La* zOPM<~4O58>+coAZ7FieH{>~qGTO-;?EoeM;rM7>>(UU+Ejm&!Q!YOD-0kMB|9;N-x zX+3nL#hW6Q;P@ZHtVo9gDd=Of_|?(?@!~>=1)N{nf>fid7x;TRG)+~bS&7Hh^5(`_ zL0in(f6H_Oi3wNuv!rY*3`!P5`ibl>*8hhGkV4SqW`lVC zx6m~DhYHD?Y5BpOD-JNI{C3Qx%8zK3tn;eq!&JfNhB1*P!OVPE_D|xg`f#bC$M*^n ziIl;1c)NZ#n>^Lb&x|t8b5gKpf)H)aSl6{lw7yGCARDvJ9;tD1F zF{$TZ<~&IKCK#ZzO65?8m1eBi4Wf66y3D8dhQvG5^7bm(Dde?HeLoMARrI9=QMjMu zZTK`wdkN2cSzZJ9hl`Oy{9kOQW^^LxzbPXaYh;*PCp87v;VogQzCOn;@^dRO3)B4} zW~REe#60vz7dZ#rlO}bYF2V!nA7sfb z!L9Pxahrqw>66`>TzkKpw^eBR(RT*Eq-spAN|VsDxQmrbzx*}O;ejnR$LGjAPZDZq z{lbcDUnInO&V3T`*W|%tb8M8kpSOc$RA2opLPXVP z9U=#ofZq0lp=s;I?HMWc9+>H^^>@&Vc-DC;PJf=AKdv>!lCybdP=vh!+8Mk}I8Jpu zTUTO(5n?K~lbES-ENsOZnGVAy(-}I?ot=O%1gUqd#1Ja(&}B#Z#ai5Z2KIT)*i~BJ zcS$oMjS5_xnB8%l(ve_GZNUCYUfGa5<~jZM6+_u$FEGS&(MvY=v0>lU%0n!CT@bP? zhC@y7GmYh7CX5^jBwRkGfNE}Aa?#-;5QAh*L#6+v2l_>k#;U|dxBq(&`5y8;58M#LuHC5^$aN~e?>+PIqwq?5c~;xo1pzLMyW!XfUzv~ zLRa=XX#XgRLsLOLB6$H03U1M9?~|;a)a<%IRpio2SJrmC{no9ySrCLg{a8OF~8vPVq=GRLB8Q zFysnM#+q;C#Z#tssj0e?gTX~ksz>#7$Ft0;(gUy9ilVlCxvO<4al?1_&h{5PRf~mp zGWrak^4OS%?rr0HI62h7J{F3Pgu2b;9`MDp%~z zd_!}tGNLeM%w>r%HS?ME?=LI!)`B&rLaGjnJV9Hvis!Z0FIIm_nVz!0HY9(Mkc##- zu-O`jHCC`9j)hRy%&qQwKU#@Ad!>=+xP!9z^+#Y9T4TQ(IoTU-U6go@kXRRu8B8zRQtfM^`xWJm3quN2?-t2`ZQCC2uc(*qaQmT%kCA45K|? zX7k#NYPjy@9A;MnH&K_jR6L-axvh)%lJ)MoZAm7T$sRw#HK+sZon4 z@98{Nfr_E_;~@~fQB@OgAQ=RHf1p(E97maw>?7^Z=zyVAyg*kegMK8a=LNef-;7I4 zy1^W;^NK{$s=k>n$ZzF94!voCB7l5Z9ce^t+xg%^#7P?fwpr0;+;FQns=?1Axl-o+ z{%6V2Vk3k@HwEA^ePxAo3&ht z35?srR}Ag(*x1Ce$JPlmEbJFGn<3Ty8P5r=Hax7i$hGFw?3Okz5Z_~p*E66OJvLv} zgmF7LJAuUYYMkZ5W~%2GOlu?*a_OZ}_Uc6eoN>Iv$RDh}5Y;k~;pVQ*W=6c#ceC*7 z)N>hki@cX;RA1|zPrqGZ320O2sx2RS2q_wITXw6+@ zDu2m$Kwx)#t?Ee)4P)AvwS}6{I|I5YlqQKbjET1@k|LgA_nul6B@w}sD6}gFz1zd{ zZLiUUr_Th<+M|3&lZC{q7^S24q?V=OvAt=z7nrydJ8NdUSto1XjT&o!Ou`-pH54(_ z6TzpjAox+Sc6mj%pfKee&%nb|k%$9EuIKQqQ<|dR<(2k!25a^&W)6K9)lUeXm|IhP zy*IIS^Tv#CiEXOiB_0m+nWK`OxhiK?P4VJYF#dCK7$!!Pg>{`p+r%qy66YYcP|tE_+MnJ_=vn>8kE z<+KCedQaZ+^MlG)Ety6!31INSiQsM_WFW}GFVVnzU>OW(UQR10Q4f*GGnPCzWTEK0 zcoRDtGKtiCa>gSe`sHSL;m4<&1)e;%$HfgJ`NX7jPJ_pC`oay-CZ)nnccXF(y0RpT8R zp4i2zU+cmvV3*Gm==+u~wGhv;DM2LOu)+_=4G1ZLq7{Mc$DpWqw#nHyt>LL0<(*=v z72|vO@e3I*BpsPXEuggIlyeTMYA?eH`N3KruWT1nRw$W&r^MwTR|^LgDK;S}DJ!k* z`ea4_ukgmsx9(J1E*%$fZ~D5qu^z}D*Dp<-vynn$ z=aDXxsc-Wphyo3S&G0=1h@x3Ny$bFRDC|zB$PF2I8Mw5Z`n3JR2HS(hUKDKvHcmu? zZLDIHC!D^ha^?tLrX4C2S7e&aj?R=08(k7B%Jqs{&f~3@Kp5xO&l?eVC~~?FT%p4}EW&|%E<`)N`Xei&x!H;aOl8*C?`l~~u2Dxc zCt>Hp!N@bd*u5(N+GQ$q$ef)kh#3)wq-JJ@+fRY01WA%L@=A&b0tckA_nHIRFWhpT z@qs>=C(%0o3L$3e?Up7_d!B+W%qro%y7_}If7l48Bq~VGov2bI%rS+q)l5^hf>%;} z`d^zSB9xVDqGL#^N!}hT+!6b{(cEo`bbCt(?h-TQn9ya?O{~^v8*Gtk@(PIH-x@&24p5vEmLioB`eBDhS<}GJ z`!qH|BNOa{1^<$FIoA1LXo^uF9fcMYwRL}0jJ0^WZo{`Tn-#ONP9u_s1hSh@U&(Z# zhM3q)*o&WHDxnai6kyTVEQN%qp2jnzu$dDMuoMHHr=ah)jqCGNa*EV z<<+`jpSvD_ghadCe=uWksv;Bfy9z(C|KQnwq1li$voP}B{-ifdnUwhGzsG921n+L| zs-CPB)Ch){U|+MtzEVy8j16z+Q9JCYpvQ|z>>ANLLXOUUQZYqA@)ISB2YBnAC~0lN z!3rVgkequJwtDeiO43jN>L^9*4;bgaJ!3ZzGvS7qE6B1CdyVlV`$^`t*q*`%r>Z-y z+=dySOSz|^O%6&c%s0Ct>-6mtm9#=~;Hl(EMr1qj?7j`nuFWsEq>r2NhE*`cDR`2r z{)HPcv}C0aG+V8?_&H#+%^F6m3x?gK;_2wQ2z@w{tiuPW3Q^Ltes!k_Zu-l_sqUq< zmsuzX?QlC#Y&K5?wdElAIxr&&IU3c=Ut{ugbuWyJ<~0z1Ws;d{DNiV7sdOBwwa>JC z#L#>DaeOEn?|bH+d7A*(5?wDaui9Krp?IlhBG||B}dEoDj zO;|d6@mRd6p9Sy2QZpl0s1voFF8cVhzH~zGyoz~w)0dCYMD5e&g?buuLWZ4=jB?as zpPuzA+>M(hQ~GME5*8ax!c}ev63)zvX$ky`4Gsf@=(QoomE9 zqo=ZU@tj0ezTDjlY9I9isQM^H4fGfEiu?`C0Q;XpJLz3P%eIj_f!|THMNg_F-#ea51+a`d zf3f+HSP(aLNkRC-z9?@|($LywS%MaxJM7ZrLHFdA5Z3OtS}S z&P$z&59lx0K(NvgG!R^a0eL|vNl_?I?qu#WFDnhwQ()1 z?<5=Qz#k9+&PD-s1aygRft}+}-E9c)ZLqJ)_YfHtAsx2ZRN9#L)pMS7r1>=}WqfL? zbZ8077as!ljIoiL+?^!o`lKQ}$d|zhQmH=1;k&L~jRjo+tc38H&mH$uDeIvCFFF{5 zAyhMU{rix4CeQiF2VAL3tea(?MYgF?0xdMnfiKhs)GQ5YDzBOsv+sG{R^s0zexqnk zAN_e`ZfjX zfm#!Ywh^K+clCE(XP#@t-G7`%G1RQ-#n@n$n7MC)9b{*DQtGsM<7`8{Z0C5aK&px!pM%Xu~2@dfCm= z^%5b;i=zjRs9{AC>pqJT-6znz^M~rhlctF>6Y#@iaj6UZVw>FnQ2?l@&%^rq%oJum zD*;mWlIeY=CpbS+XVpvB=-!r~QlmSNMw-6mFZ@>9Ce>R5p4Gx5apCo54*WfW54s(Q zCynuv9xGtjZRLB+w$qIZ96_c}ij&EbXRWh26uS}ly<(blMn_llCWkJc(K2_ z2E7POF{Vs5YA!sv!YQY-bX!$n>_Zr0Gp~d$1lFn)$r{cGB^#lPnO@jA1X0&;aVaNH zd7$*+_91Bo)AY{ z>~tJQ(d6_jn%c~}CbwERK|ZVJc{u)L(gXP`g~fri?zef3)$FerTBM>k&?axTmx@Gg zSC7mJJY9**%IRUsG`G>wyOia(9>0vM^Xz9{Q616I8~iqE0t9sHw1k-mP#-P_sWy0Y z%kx1$*>C*pJzrG4+)kfj@?0;AJAXR69G*uP*rc}%Zm`ZquL;E2quN;!kaj*LE;E{> zK3bnA0nmfj;piMnDeznl2HQUGAAD1!s1*HbgVL6G9MnBFtlef7yO(idFyl0N47Qyk zb&?Zum?y5Mf{k3*O4P#qMvcCB&&46=X6BGUW9YW^Nw!^Y3ff`9XcCM-(i-6B#%^7Tru zb{~ejKf_7^xyVU(xj^Q{G#+2h(C2*}i)`}dHk4@7UPG$AN^va4CFMZ8GlqvvtSBKC zm0ivE>CbLXze@A2q8Pm(4H=|w3z0fC$Q033DEvrUC*$h0v|87N z_%0B1pwQafepK3gc1t~RW^B(mI8;`bXpWHR2L-^39VA$=SIER9tfmP6#;4m?g3ODU z0$GGxJcHzHPNo^3_E>a0X-SdK9*WzI%p%XAlW7=v$H{_2aw6AkXIq>+=Rq5;LJ79K&?M84|m45eXR{ zMPM}<6Vk_HEVwt~J{s$C zvDXAKu4juU2xq#-)3IOrXqyihfIJZleyt8Mb_V*%jH$5WqkldP>E&%^`-HG*Ch!xk z9xXRWw4z@UH$POTW-(x1>SSG5C2N};%gYTCF(3Kg*hJk6B+gio_sXh_Dj=f#DW06G zt1pSIu<=h)yo4QRt_paoW70bDl(YD_xNS|t;Jk!=LPBm&?Ii{?K)oFHT1vMBx)`@u z;Pp{3e6K?xbbPZ$a;O7FVP&LoXqBMbVbsb&SHAlQx{rsu{n&pv7v!ae-QLnnGDa^B z^rjAvxu?nINZmiIx1$W}fIa0?(W&24-g{#y;P68u5oMr$|IPJth!Z0@U9}@QTN%B0 znutz-buH(ckBp*xLeR487j=AJQHk+Li2{1Efl;X9v(XFsr+Fue&CdzUWMa_8kI)4> z-qDnA>rAGZ{lTWUiz+<7Kt%@bTv~sasQYM%QATN>$wiYo{op$#hO`Q5+UgA#hn)(O zO;e|g+F~i1HBnyUamt*!@9H^5r{>L>?9sau5IhnPKwjCnnCxL%v@kIU>z3P`X7vee z{|*t{gb_}PsfkH%&W7$j3qcm$hPe}S+9`xaM$*v7CGe zcR*scHk%*vQ*y3PZKiwaqzwDUoDty^nKmH~%sjllZJ^<4sq$sW6LLP~YKv{P@s_^; z){I_1iaMIKvOk#zkB9QfAWQ%mFMh%n82BLVyJZYdQVntGvk;x@M>AS8=H1|=>BoAx zFk0C;WbFoN{~pYEvjIib5^108#S)>Y(f$EGkbK2xt@M=I`VEut3h7CYL+YD2x2lB# zmCoEY#Ye5iM*MwIs-XIxFkaK|&LU7q2dxOJp__6l6g(D!l~KE4FO@6JQ3>U;Cq|$7 z_7LtGaOE)p*0sVNT}Cb^@FO;5unHjM;<5Vk9&Dj4(&IsJ}WUs?5}zM%-ueZ^hgI3tS| zw;L^H{0Zn!bp+y=$y-RID&G z0|J5%;$P0yC&hc}0)L3B1KlVG5$m8$#S=-!OcX&^)CN+c1Tbir$-O%lV zkW_!-ro+6apoa8@y@hqwc)3VBpq=*k$ex zWDd!HX*zK_gB0f}>v!+SXdZ`7I6P-V(eDcJ8SMo66yr^64sM>+4c*;drmXi!9SA2W z;Rd@8_YqI?XoP&d@wrZexQP08+=;i0XTe=Cm2?wm+5R5po%w}`b;i^)iqZ8EVPvy6 znIOY7neNPf%Dn3^c2kO2MeD{@pt?1dZDIr8-L!xa2p8qJumKvxtNY{|XqKyS?|KOq zpl#G^yR;A~Lzn73pGAO2d<>Hvigdrel}8&#{5{b4L*q``Bb#c^Ix5vV8mJ*0*kqao~ezp z%y$NY0y}MP?QB1ThpvN-Q*ddz{IFs7jh8TuXRfI?FmDAcKAoO}`0Hi*p1fJj_73_* zbjLxD{upSJxL$GhVOUrf2tn{Kw0?y}`Ux}0 z)0-mj;|R$fks&bV&DH`bZ4M!d3^y5TQdvn?>M!$$l*-3y614t+urFZ2zrUA6&cRo1 zh0cU%TkH}b-0L6Mr}k56N-7*vQCdpfOQ6by+a=Qu*{TD&Q*EwbE#L6#6!1Hf`p_fL zxXNdPG9T-E5i!%jXJKaD^t4nXmg=2P1q+Y5@*Qzp3LCj{`Cu>YflOrro?|&rVpsjl zNgR>7w!NmlP2)=LM(+_s)mnVV8Zi%fZ`=@e*Yolx%Eb{R#tr@?BqOMTA$KCqT_#KL zq@^!Mx69JJ@l|u7w*s@0#*H@RnqD4Y;HN=MNnPRb*dxYJqC#~#x$!t0DUFC5aoV>} zd!fb`_jd`mB>0jYEU;^~ruZuJ{%o)Vs_|fJp3f0Bq_U9;;ooAToO=x7N$tw1^D_T&^CX~Bb~lU-^F%Wpox?ocrJ;RQ&+Cz;9?Ii zvIItUyv#MCdcX#Af>D$D*})Vo%UpMbTg8p$ZVmb#xe69FgG2~67^~cZ1;a6(&diLp!*S~18_(C zzb<4iJ5_KjqK^XkXM->4UDU(^yhQfa34xx; zluRvD%oe65kCf19A#467`1=qcA{vOfSthA25AN>8G+zafM)o3w*a3+~P7ENssxIxy zsQ0a(oB#s|THm4eFJ;1RGc=n^QL#p=)qPqm`eg6vV;EfxVapU1S2?;+$NR%VZ;u3v zK=2UI$PtfbB#U@hW%#Bt_V-43<@W?K)oG?+R9sGYG_NsGsN~FeM=wVry-O@rNL#Sb zy@z=<>#F_od*)g!d;ud%e_8Skgx8- z-Cg4dn%uGIjwu(#zgnetZmvoz2lJwYy`VH`O&`G_vs1x533#S zgr3kOaypSW+pC zviGCVX)q*ZdjX3pKSMhK>R~anK7>`Sa+tWu6RFL+R_jG`&OP2?QHp9@hpunxq?>Yk zvp4)yPGl$mB4o_qG?bdv%iK;w+$=hzHE^H$j9a=kvbG#<-acF= z6xS`y4Y&%auok|kub&q0T$}gTFxUgjCGNGO4J+woSrqUfbXlCth(-hII~oK?Zt(!` z+SUq3=|MaSEb?+^1;;Qi{0+iCiOXvQ>RGRP*@F*b!YI}BgHLGwxR70i7xuH#`nAcHaTDB9)Ct`qnXqPn+?XR_ zC4Vt-6F0Dmrfhi>E)$iM0ChM}4`*%gaZbBzFJH#K$Xhvw2S}*naSMxerX?$agN>+9PCtQ> z2a zbQDM)G%sxx1{R!_@4CJmVvK-@FixKox$*ljrJTLcXb_bg=>3ueN_67;BL(WW2t+yL zKweYaD2s!gdp`6_I~=BmDTV<@D&~lQACP74{%ov}(R36L^06IFa_2x+`an?{7Sj0N zzG2oEa%>ZU&JsB0?WAAD9i?4=iXK4JX9x6Td%d7F=0c|tOxFYU-$Q0y&g9o3&LWS% zTfN}8MIqZrj?SxJfYQE72R}qs`bJu>OONzNDC|RFa>z~Bl^J|~79=~z=s;oRijksn zQC=E(w+NGP4C7L|ASZn>&`Q)yM+uy+;JF&?^_6A5B<7B*aXwK{F+;VIhj2K&H0I$K z_`vHhMyqQk?G$Y9r>jB8;APR&m?-K6jqOyg#s~$Jl-}U^kxf6i z5wIc7=?xewwR-WWd<>UJ6)Pw0Ud2P4NVnNOz_UYJuidrxaF^gwfFAdIQ-?^OIHuBi z3%sfg*~mwM(&)5lNA-ng3+M~u-nyo`sGSfGg6%EOJmn~;5U$v|7Uy8W=&ZV>*QyPQ zT)1qlug%|W%qEU7<-T@!WvaE_Vyu0u|2Ek4yWa(y5${ML&h@fKnhSku3r&4qvHt!3 zsjBxPK(qaF&wrF%2IWD;)ozglzQgQ!VH1PjiYzIVwrDc+cia3rysjWir9$NC!vO(gsWzXeTqVxLaHGR%TeD`$!+PKUad_r zFW^wGS{8-(_?&RglfIQpy*-K+NA26}nMubBZNew27*I+-CVs~tE5AI2r^WN3`;7om zZkKF&f%KWGofSVPRtxXlHRoim9_1xXCV!aD-8VBi9s9O~7hUItumZ03GD^79b&mo9 z8IK;P?ILXweX)-)u2T59@JGE(Aj=|X6PxGKq*vVsG?Kx9bFqLT%Xt~5F@M&RG{WQJ z$|3@J#w;k>!CN~D)p+o)3q35YdeT6_z=hBIgh%M`tWlM9ec-+J5QSR8>h7_&CU8oC z`O9S$K5?BlErw4HutwUJMCse#9fP7Felos|-|~9!=}HGbS#&oQ9 zQ1x?02x&f!H_SQ*_{2U)8+{8YD!>M=KD0q2J{+o%qIeEwRSpqHaRA}315vzGl=j_1 z)0h{pBt8UWB2!}7TT+kU81y?v7;#a8MLBk!>5GjaDjIsn&m(Dhlm{k&$w#vaxv6zZ z{ScRGb4?!?%OH(3OlE%_%Uk$4D~0MQ&**HDl2tw|Yr$TkrMtk8!X)PFUR(*ZQ_uA8 z_%BHyYud3tQwL~6iaXo$nn+pYJ6g0Xg*05(vFXOrjlzh4A!16PA|ia0rD(w2>Y5%H zbdm$OZJCJFEErhrN8tX+Z3_~=9Jf4{JS^9pmcR``!g|RZD&BkV>F(}}5k**ekhU(w zMA?;vHE#61Va%z(GA2pWX7HsJpf}GJYzvmZn=x;I?C|OUSpJldV}#w*E;GSIT_b_W zrRfrto3371zMWtJ*QT80XEjihI<$zRBavhc*W*P{l_o9icTuc$;j(m$#sOEyh1HsD z#ItM*URjzyy$nbKOtyGz3n05klmuTcHa`(~GuK2OH3>C{Ci^I^Pn>YN6#sZiTAr49=3kO1&_G8rXz*{`c-=P;60V7BWy6{cqAF)o zpzd5n6=U_+<{`m$y;p-h2SU4$KZ@H^$6S|izkz>|m`4MWo4~5LQa<$I-a!&xO0!e1 zoG`+o1-IS6T6@<+V*1&xonUdl7gAO$*D!DzSnM!%Ep$*`;ByNdlM+hxhH_nDhWEI4 zZgu(*kHJd8GB3IE$i>){2BM}QtA5iZ*H|=K!h49woGisrE%+Vt^U=4P!_mI3t}0sv zv*EGE1&72>2crt?UAVY)fEWrTH65aWok$MchoAFf>^D)}H#*LYbv$Uwt!AqgY`|wO zrn(gey1k>6nUPOyy?KYag6T746-`#I_;hp40><~dq4aeYMan!{Vu6GG6-f%*Y>CGM z|HzS)Z{*F27r&;4W?fI`(S|IqJvHvD_e@JOk-_W|@O`piZ&!pupZ!JvGFl!tK}#ud zszdaNge1t1erW*?JQ7m~AE|0fxgpv(a@(!x z`&9dbBwCA)U(E{^;R0ug)rocm5BjG^#@i|>r#L?m&N@wN7O`FHfQ4REnG-G2jQ8tI9AAvjYiqpinoSOchf?k|Xeit6s@jo{)v zY0bXE3*<%#B_si2(^h=z@imabid-o44+(1$0~#HNV@84PvH`{LU*?(O{eeG* zFBNVvn2|bG8TMus^n;_L1odaQb{Dzs<>k1gk>qS~LjJlN%VyPFNV>7wa5wP&d&7^| z?%9sZ1jVPxqqjxkjnzDb^o82l)3{9wWu;lQk6{>@iQHA&NBBkE{LEOAw+FJtZ8k0X z4W8tFe5zg$D!wbdvs(VJR-7#E8tYny)>-CTcCS616vRV9N)Oqnb@|MR9FM4TVzl4H z8WD8w0gXGtkl*$s5Zx?2QZNkYaYfthTF!pO=E)@OW~?DBX!omYgcAGJz+>P>0Lp|q z`!q{&>=+zzki!t4$)6}V9dFOzEY65OfE*uV`CgkjLLw&&$U1!8KZ!k@6Ct$xD6T#@|VA8Hbg`j4Fgp^6f32n^O03kQ9__-7S(0*y^J-5_na;MGw0l*2 zf}Y%?R0cGZRHY6bH*S3!-ngGzf!r+{erSu&v_TZmT2;$$=Wcu5tA8wJIZ`w-_U&op z`)DkQm{FVIaz!UZ-4f#MR=<$MqG7L)jbn-mS8W~EBK;jcPdzsEfj|9`bmuKPJ|+aY ztf(Xar{VXyEF6y(&=~TYO!)1MufR{e8~Zy!oAaWnI0X!=8r4rkqry0DePt3@jW`iV z%|nK2xT)Ae`O5CgIi7x_-yD3e%xoqJUTvDls7i}g@eyk5 z4{kE)AsD9!Xv;dzZH1vSlw9uU1G^H+EiS-f*>~1o11h=hT;YUV*{^pymDnPf%j9ID z_vtDTx1%fNjU_rvW2%b^$7ND|D6l6c*0G`Dfzh^*+ok32`K=$i2p9H20)*`Xsz+y! zM(7v&SV~^xZeeA&AlKfV3Dr^OzGttR^$+`0e_XsyF#;@+=L7k(SIqMN$U4SdLm7ae zZjSJj)FxbJk0X98U&bc~>at z5m006xWBi)u#g`EB3P?1+?_mev-nFc;0_Y0#Al%iDyCvOv+g}RO4uU{OZv_s(vzA` zo6^>K&r>@JoSs%t3M^2G`a)(3B;hMHqds?W;I&Z$AM7!JhkJ>JB{c#I5`vP25UUCU~r(DZ0l|?ykFQ*V#dsoy%)n zgk=1gmTD26I-qJo{AD5|oH>8e2L$4RMkwgsms*WH?-pyL1T&WMCq`9TZs(@0Kg8x4 zOeOwqk zV2^B6*CL3$XZqxMu694G6lwzrF?E@*5;*67A^L!)+G`qJooNi#Y(&pmNqgM3u08o? z{DPwoS74B~L8W1<2njjE=uXhI4V=V)9OVG@6SxWhH{SuNdK*LQ` zBj^asNYM*afD&W$z?{_Cxu^!r@bCZK!G`6L1sc5ZGi@4aRx(wULa#$~w_833unfiP z(-obWAlM!tJZ1#EVJGB#cLU%rm;yjx(Bog8I+cSu!=ty(WwjGPV`gtU;{k)yrKegV z+)+WwLt3XIQQ+?LEqVIog>(sNh9m`K`Yt7Zw%u>?AP6c|yS59QnQVn|@L?WOyxx72*IJaria zJ1v%7)OF%?ugGmIaMJrk-Gtm4*>U8$(3u1khoQ-WOD>4N-|+W7zjEc($!2sVs7ZAX zDJe7VEjRh)Q5QcXD~O*d192+mUbrHY_LFseC&`QKKVRj;99%0Ph*HTM-pXVN z4rOnCf)@UQ(~ucR9|u@2xJF}F`o7;B`faZCqm03L0IrgK+l7g_Ny!^#@0i`1Cv7d-iZENKWB z6muH<=>1u`qb}D}bt18ohBvjrxLt_N^z1ykFGpCmsC%Pq>G6|p;-dO111!QSc@(Z^TZ5?grO*YshB?hhQ8t zJMz0$U|gVHScKvu$~I!amtleKy}5x{$+imQNn0TWzUuSSQTzvEy?SPJGp&1N^nlHQ zN9lVP=%@P)p_p~G0E+w-wLasBs4cvUwe30xXeYDnZ718fMJb%^@N`+>`@AfaHEvtE z%gDp82y_TV6_*F@(eue#43On*}rrF%tD_O1<*O8f5^ z$A{tSt4k>auIEK{aRntW4XxDG=FHv@P(<|FwU~u2hKhK=z*|C!OFn3$0?^Oi8YEyD zlqeEHW0&$@cbIgt()j>lP6E3`s*AvGljRj*Tw|rt;0N<-Er8WM9<}>7nU!FBDIMn;BLsRcVyAEV zICyq69KGH~Fjysjxaj&gs%Jl)TDNu&Avw!OkWa!L|#Z& zTj_u%3AL<35Moe_U>cd=R#oJ0vyw2i*M?*5LPz&n(Tik0LV%l>`5@z8$iqYQY6*~4*E?(BaZM^^IRDz*L^$u3kW&cA`L8P(%4e;XnNSQHEy z=nU-x!Bq*WW(RecAz;jA=7x0oiEL+rPZ|AXvvrel_BW*2PZhV!x^EYuKf{|VDB`bO zg%F3vJz$FIJaVNrMAf=(O-mJ>UY(R`0cpoCV?61U{lKevV#(d_B7*st7~VEH7enWRzH(2hy(J_Bnhrx5^ z9cm5fBh|g^IPKMfWCVIf@5D^*{vok*WL_l{ysY{{W-ttfN0y-0Cg2#oA~z5Lb(C-I z(X-Q*2hB-Y72u@7`|w%Nhnp3k#_cKHed&Yp{p*0=GrW9X+oh;X&4iig*(zsd)(X*s z9{le<`hG6+pIzdtV-r2dYlj=fJKJVpGx-#Kv5s8wk{o$z-I9EM(Ldj$= zTAFTUsLHL?8n7TdY`?88pk{t-_}GBTQL&2_P-lha%6(#5*4ELm`j-TC=Be10RRA!n51$G ze^-+C46AVcRIiqtlBN(HTL_kBz=})>*j5R>MY!Dj>!;s;W7vOHJpTu%fgnFS0Hzy) zC=iMe5C4&g3qj`-HCO+JJn_x0000byUy3;!{=I)&Xp#Wn6oozG0!KH~xQO;pjJ;=c z^HDJ~Prv{)Fv)}p4-DHgl5gG!?m>x!1Un3rZimL~P}vWs5)oYbDOy5IXYbLP25KVE zqE(DshxXjV(3CA|^UYLZ%zZaQL%>k>4x4G@FGw=<_U?<&;Tv4qvVCR#txCi(m?juV zaNM1{YFgL7;GsZmUV;B1CCAN6%H5$Q@2ixxq5uj$e~NwuCUgfLY25XzKoe^!jm3 z0Pq`%ElH^arvwef>y(x$*wZcl{$FzND-@(jV12W>nCEY!J&7ilsL$}_sJC5DJQzmk z`8WLjPR+`q*mWfKy_wOyBt!=XOWOzjaPkg8$>Qg+-z58iGJ>rbn6#_VbPMyT&`$R5 zOOGv`gHfck^gom#6*^RUc__Gx`?cJR0EqBrYpXk3+TZS$u(__22Y74#@hD}$oC>E5X`md;5K%H zDz(axH%(nLjoQ*tD?8jcFr%7%ji>JQdN`+^yu;e;8eKZmlopX@?APkf724J zvCB)!xU6!$Z+tOu8{=(9ii7?{!!DYVIBF=%3F4C5-jzI5Ury;D?se&S%HFSKDnbD< zgmNi!0Ot~h;AH6D0j_8a$Zc3#Jq%u{h*!ujXs?(qA0UNn+CW5-f5h4{;@H5ICV)r5 z&lp;CAN*|>#Fd|z zpI7MDq19DaPH*(l2O%t4!wW73`}PJuNNN|fhrRxHU4&eBmKh?%fYv2v1?bZwcXu$s z{ZCltn<%sS$liA)9zVeIQhA;ZDOpPJ0!yZzGaMgR9-C-X2)F5vpX0-Cg)2+-l2YY{ ziKv$Gj;S;}dgwMn(*VJd&?0`xV|Klf?JMTY(qB*?K1-d+ii8ze4&gLyY+rr5m{!n1 zu`|=c=O4=jO6jOuX|8XQk;zb(@dBKfFjzMbH;EPzzT{ALs=(|YhoqLzw}`!Q7ADKUjuo4%e>c+|jKR6+-?Dd_+TjrC$Rj{b+$7V)5UFV= z!02Okg8< z+KY;K4%xT>jFsOK#=GUywjywCq7H7tg{O%nDZH8Zsp{q7oY#fE-BWJ`8Md{Kg;e>C3NY4(QKhk&3Oxi@tH@)#H?+ z1E<@slG3>fpW!lH@{)H)q@N*tHWlikk%d%D#K!M3Y^D=H@_~OYJ~nFI^^MWM_L3n= z4X|?rFF6Dv5q1E8`SVtUO&XnYw@crGM>M8R9qH7UTo#3|f&EE_Y;8McRqj1|{zw=W zu12IqhNzd^effD!j)^eb)C*X~@CRn;Q;(2Y7_yf0ED<1xA3MDJmq`BCi|-Qn%`2w1 zWk|KM7|_yVk+ks~4Ss8(#;4i92Yz7f#Vye&0;865b)i6bszY8JG%H235zD?+F*puk(m2iala=?;JIssa!hDT9DY%X*`A zLL;ZqLjWM9vjL-X>s4@bo77>}HeBk^pPSFBleP=1#D;3~(()#U>Kb}SiyMK6J$mwp zTEeMnbEgUHuU5%*E%l~5DGW^!_l@>vj6!gbKIs4}E;o6W1HC{ER!VVLi5SQNhric>0LR#{x>t- zV;Y`v{?TxSTRC($?_UEnWeW@CVat}vtZOT5)5FHcF4Kz zS)e-tJepH>w@-oLc_pPEw(>dMi<~n?I{XkP6yPRF!vzW9VVD&j4dQPtS;`@T_NE5~( znazY8FhNs)89T`zFU$;Dc2`IlB#1tmfZAmU6IXG~`;5d_ZqX5WPVJaXS{%}3sSg<@y}HpW>iV`I__daP#k(S+gT{)RQC%%#c^BPNzMT}1r}gUEr<SuWgJl;=*V{z7uAG~bXy9{jdn2g1Kjx**-9@5b~yp?XWLJW->NOF%PUaoPNQ$D zK&BIjw@@e+{m4>uON>q4-F_7?dz;Q3ULv9(kg=g|gwK7by6m?gTm4zHcbp>Ya!Xa)W z%FWwv&@}coF@^*~%GF#DZkEGX^L$B9hR=-LA9Y|N`3bHWueB#nb8ZTr@=Xrx0W#eV z!Ybc+xCYPTGL9Pas1MH&?+aJ~P`G8M;@ICVhwE66KW!Ge{*p`Nz)yLZ2dltRB}GH! z4d?5w!lIbl*qNr*evupXm&b-vYlG1VgD7~TeKq|PQ}@#J6{YZTF`U{9hqJa|R#iLW zSczDS`B+QFtZ0aSD%7o3K|2?!p&W6E1~OZ`hTV}-pm@yH<^v8SCAl=T zrQ7U?w_m$))kXe1W)m)WtBZ_j$rA1OH%9dqYAHG1Fpg3C{3%4ZK>9-5^RI#~@Nn8= zep39F>my?KEGTSwf6m5$tZ~p-9#yGgHzbj-`NPyfi=Zz@-=o-mV9~#(z~XWcgzcv6&bgcVPji9Bn#_6AX$YQJ^BLuWqIO*~Pxzk&Ab2#_ z=LuYGoGF56ZP;ErtNUnup_{*UU)qZ_1AYX1=EJZ7zANZoSGa_Og61s=m~wR$4anCX z&EW=(uWztox&^#`KM2(HhG-zk?1KJ8VWVM|_>#KV@T3zYPzalgv;hfwhdjOTJkC-r zedF0O8JN-zQPJEmFXy=+cNH|(JmEikI!`IyS7&~^*Ndg2u%k4RC&5fENB!REptzYO z7}-J}4z0zu1=%nV(W=s@Vmn*vKaC~|4$&a6FPeIJ=50}9JE>xA8j&WPv)BzAW)$q* zORXhQo9cz_S_#l19}|YXFh#X$=?J+5P(;4~v@}3+I-Fol4*_J7$Nh$tmf_hAvuu}H zUXAK0*xFo(_KIceU)_cM9A$FFHgV!#%ZztLuKigb&^SsM3Z)Q?;W;TtKKm9x(n#H% zx0K3D_i*^750-uAo+|af85j@df$RTV-3bI7Cn>V{lvVw`KKM+sRG~I$NZuFW@g%A; zA@CL&wr)wVxau7eY6(F`EJJF{TA3YPr833TSA|@`3U?NgUgBW~Z! zux;C!ZR+1$5nAXXki!JA)O4XOmCkfzEi5Z>z(?8P*G#u%W_ zGuOQv{5+WWV^(PD4CIrwJ;P!Y4{SQPC|Sf%!v}UG^043I5oE~Pus_vU*&S!&4m@jt z(**5AM~Sed@Vpac5A1b2i8(m`ND0HCeP%55L(`Pq!kyQjSPy1s>tdEp@cU@di8i~< z#mk8)(2R^(j-E;o6=R0NQLaa)^n3DWvvgLR#XQ6zTQ#~C;`swC-xN)?$Q7MV7Z%ZMLjd{WZ+Kaj766KM@(pi&ZC8jx14%mPG7y=1pw@A|Jm@L0 zS!mnVjQKT1a*PCW5@i8ahUj@u>fQiqF!8!uQ;Bb@T2IUd2Sd*VVv&UBm_4=(n04O| zH=#QFo1?rZF1}wbP*Y3;g~L>BBfrO=%k-w=+=Yw6t*Q&56TBoLXq^EF2N zk|d`?c`(^8euTS#rVsuD4FK@tRq$!|lNLJjXCo8H;L533Qpa#4vd{SFf{@bpgJ+VA zi5#kLx>*{UP(oP3F{!MP5jZ$q`~W*$79msi-t=ao5859d)*dy5QUQhZ!4s zs?*Oun;rdn;jC9L`G$LzP01*vA7THxyIOe&Xn-qZhfN!^VuJ8a9mvnlu0A;W4z~+t zAI>N6#Pfr+uJd4^2Wyr=V$Wp%%n|mH#P0M3ykDj=QwY_J4}#^Vc_-rc(&Mt=&4iSD zd)}t7C;OZ@sVGV>D1A)2MdI!^yPbp4C92vGY2Q%)@tYTIqFbJCF{i|d*yCj6CvVPz zeopZI@YF6g4SUTeeP6=On7(FTJeZN6A5>c$;brh9IZZeX%}87WZh9as$BkO4j999P zjsdHKCBL1pw|>Vdco{ELHwi|peAqL`)BSS;TEzGR#;w#yR-ys)PZ9JwXYdtou`;}S zU{dK2TGG0UfNgEr*WppO47^n)cM^2-TvY&gnyhz@|p2NZ zKD8JS+&^&HW$bdkfEe)k)|n-gg*yCrEMj*$a%VjL0G3~u((a8mff7{cipmMhte;)A zcIKktOEbMVB5(xxQOKxRF@^4FzS|f(hi5ek!2)S-N44lP&A%QL#n+KdO5k2c*Rcsd zGTDs5eEZ+Z0#8M?TyiBhjrQF2lX;mm*?MBb2-wtjPv2t^!lFWi0eE{V#Mh55!8X6l z1Fs4qtE)UD(`Odl=OPJ4H%HPr7V0}3Aqf1B%k`P;nWle7_=D9&l9dz8aBQo3^MMl2 z`Z}br=2^U))^rY)Bx|D;u21X#@KsOcS5psTClvI7y9U?Z^3;6b5@fra3W*O!$RH1> zf4mt${-c&X+yQY7f`ZzD?X-JsxaSDUfWDn&W)suY=t;m~~k>sc&W zu^Mg=MVKs$P70|X&U?q~P)9NE5Wg(UuwRYl0!W1#&FUOK(yWc=i9Xx|i*ony$fZDG z-HfbduCi4*WilZdp5dw_j+f#xv=SnDn8e*2qV)~lo~6Slcbv9X;QqFdileah@^2cF8N%2D%{jny+5w+@fdPN*_T`oK*o&ehlsg5-*41`x04I$7ZKl*LZeoraMv7cK$&h_V0U0J;Iw@bfu*t(*8qpJ$GU`96&D zds~l?nYS#k)B8;kv~_jF`Mw6H&+-0tinG)W|%h3SdWV58C9NFTNze*0y4ZmLgB(;HoTD_g>vwggi z^+Qk_@HwDCwsT%+0U=~Z3%lc&$|`XtBy8l$#gUmK!GTh4-|Fz%JJXBX0)d#mF{g?m z<0vnzCbCE2>U+BCPuJ_A5G9zE(kEvVSH7v_=JD-yQL-FS^c=9W^sw7Epw%9; zW}}Q{m+eucPEa;e^Y~WyzvJ&tkt)~fH!frb15$R@?-M){04T!c(ZP^7}KUwRvl~C>oz+Y>`6zuHmyER-Vk;ng}{t~7JazhSMAb&I9$VH0Y&N@4rM;}S>e22+Fih+Ks#Sv3Cz*t)x*xv1yAYn}^I@LOchPWf zalb0mDUXQzhvFItB;`h#U-W~Yc;!hy)TY#)Tqh^6hNE*k1hg6QE;pf65r9FF6wWu- zbp@;fx6_yf=6NJvbg-hx^6ZLt8@-7BjHCbh+E~GT@gfkb9RGO+x%0NXmJ;V_I`Cj< zxdBY}*FfGSlGCH!ljpG8;#`YhaWA379WCf7sIFR+0J*0_uegp*sHh{84CT2Ct|WKG z-e}K5L1!|T_00S|7~i5K`%zeM+&hfAIKafJta!J7$E!Qd)*jq(WscTt2hPn}dtN@k!!(jk*Z#HS@SgmZhprTB$^ukHvL#ERyA|0}t7)0tGve1xLm|s8c zx_sGWx|t+H<_8bj@n$kyV)WsCojs9rADV?cNv!+=hhLP;oUaU7Y;-P3rGB{ZmEZ>) z_YVvPAbgs#D+L9v+KcY7g2Yb${3-w->+H*iNK!DI;|guV{p>j&YKO~V6Wb)+5>qU^ z`sMdv2`s@?Wmdo=Y}Kqg1w|qVaP1(Ir{QOIq2I4;M!uJD-SVX1jQgq`RaEHpGt`Wm z-}ze)sT$626(=DDUB7f%86q?;_r&MM4x3c60Rtgl6}4KX7(1}ZOv60%g+dl#i#SIe zOcusJ8(qJ8jIIPDs`yo1^_5u7$>_RUfT)DWW`mgmR z6h=bTWt}^z=XC<$lKlwZRp!<~FhPnpS_AMFJjBjuuqsspkAJQM)Ys8m5GD_~y1%9w zc6tYqkQ0-cui#R|_ThZ zQnAXQ+=xK~c$q?F_cCpl3f!5cR0jH2s0OHLM<8n4NqfE`kEm1h5~!drYTosp)W=ptl;?1{Vb0^tX)U6&WUDVGFL4I&F>kxA$7ox~ga3G{TCy|E+)B-XM9^sR>k z1wMN25nxImnq!@>lPWv%C`Il4Qi0Znf4$t|^g-+kUi6^VCzNYo(fqi@kymmyAp|;- zQ7%<*&|HpNCNQUe@tnVNNwIJb_Tw>3U7{DL$FaPBTZ9K!No2VNAh9*8qZ3u@q-cM& z;jD7B2?go!QD!|E{z^ErI|MNQopF3bN`oWJg8x4Q&TQ@X6{{R8ja5(zxJ74d|G%#b zfDq5R76jnANpvp%@yo_!uFp_iG3#IZX|B7s(o|LL5c}WTH@m|nfx8IR!T){of6cNZ z9+5jk)dEt5mEwj9yGrsmtsBy zc>cDTQc;Wy-_o?G<(SY!!ZahXZxo24-){snVe~jni+5!)vnOD_#T;OeUpl9sn3f;9 z!7m2FalFa)pB(8V3_Z?>N&FMXjY8Fjw1r!`%^insZ-46 zkRgO`FCDL33y?{`2P7wuuZeTRrMchZUL2>k(e^e7tKt>x5A|=QpGxModDO`uW_o%S zcLruvGE!g~Jo{nETYq>u-)Vf{#9ipeb{aB$yK)9Fb;*G3KZ%1uOOBsWPbBa?*U~t; zJ_WSIA_M9|iCt77`^w1SPfVaVhk2JCskPNTLt7r|5yq5Wpv=TG#2Iq|CcfpszBdWx zO^QHyDVf4urA`}3C0v`r4OG`K1?hfYG?+=_0rY!3-wJ3P`_ihj?=&q18$Qzd1{5A+ zCH8m+aMu+|*`u%@sZ6h(_-zx=?%gCLU#i^J)RE;Jf-8YLBWgKUPZS=BINuov7`NzR z7dTDsdUifi*RxqXJ;jHPnBwg`S0SE+u3OJLGe=x~3}vl!Z%oLL8z3AvILxyu_Kn5Z z(Rjeywz>h0BahGrNu;`a#Ydl`RCzixUxuCpCV8NqKW!D?(8Y|?NG99arl9`@^XY;h zPVnJsOP4HYucgLNe#Xg?=o{$BD0%J@fGAPh2X#+7(O9LGu z>Y(ISpizsJHYvqNEwhkACe{@2gfKU_{d4Tl2G8YlL%zbtdWRh+qWO8qV>1t40Nd3Z zZ5$6P zWuCRtuml`5QgS)`X-wBqP8hQ#*xo@l<^xRCJn8F9OPN~_vKv4-mA@N$ihjjxUcdY} zM;AgNnxNP)SM$8U4bScYm-+w1@#cqjoLpBBwVJ3w*)C+!Pk%@N*-+MnQ1|zL-{D^# z(viH0a1L4TPKi(+|28>9UQ202P7fON+$(?dJjx%8VUmn5~)I!reA3bC8eUytj@%|#&xHB&_mo#%AgpzO<3{ZL*&hFIhrm8yKQ_IOP=fV(YPP^>mpj+T4nKY1 zB~x}(fem?xjGgOs%jpe+r=!H8kE6X;`VEQ?o-K`1e>l8xC5W`?xQ|e49P3=>#|=!y zlt$WYQQ=Iv!YCFH6iy1Dr0p9bUjNv{BZ87R{!Gqjp0$UW{0X8yfHL@Q~yFFK*a#z`k;X%DW-Bn(Lbr!5NrYI~GHuFMbEp5km0 zz1xvygN1IEW}sDtxM!I1l(|{$uDdS4|Ivp4b(QXh9>Y>O)sV_LZAr4 zMF(n@QlB^9)QJ=Bh#Nu?vjl~29nbbzG&8IY3hYY>%1cT?qZaPU;M_Z?XG66^U3)$g zu3WiFT=y%%-(`yZ-Pm#*&3OUm#+Olu_AXMpS`#)wVKRYgiw#R$oU-19-(NkZw21wd z=A&pk^ajUXcM_^37iwt!jYKGSL(ul4VA{!uv@u&}Bi>M#KHLI91kw?K1zB(rOK;UM zD7~b#m_z`Yk=S3k781n*eRb`fed$S}y}##zbq}`Y8mY+!2WJx^V0=lN_}p@2lI!t4D8rV1dwf<9z2Bsf{WVHU z%9*B8?OTFiN#?*hX&aWf?*<-wK+HcUK$yV$QlrudLef7crW2wPk=wOQc0Kdu24&j} zl)LNkjjp?9G3RfB&vtD!Sv-7Nms1A(?J1EXgrIUIKqDo1Ng3m2`=u(7t#A;0CyuP7 zy!$j>a3r5kWe@G__S03QH##y$NgyELsed|SYiq%|Gb+l!1Or{;KMM!O-q@1QLbmYx zx350@U8Yb3kbr|z#?2?!fiAS6CM;#f{}dDeNOktuuSmAQ(z)BTGzz8zm@A0qwqJiy z4YWrjtgU-E1s{dw%`D#LV)t!o5cpa~uVp@i*~X@Q?>t@Jag@7j74 z1wlxnvNS475kp1qjMC6>s^vta zy8gG+!Soh1NXFp5i9t%|1pbzJ=CwfSrzp6?5QhnQtjc;+??UwDNR%9lO&htZPfNW` z2=}b!#a`7gA-Ofk<=_1yI${=Hr8@fIcpw%%6N4j96aT?sT4G zF&Z*c6i)%YRcg%qo^$L%dg8(Yu)XcL(OaH=Ax^9BwUVpX!}HrlHT=0T)Ejs4Mh z?{TLAjo@;ymlz5m;9zw-zhtX_kbD2tVX=nt$s_k#rE_1%z>xjBfz!2PcAtOS`^S&L z|M??c#{=u+7j`5ZiAB_$Mw(m9S%Y%>zOHgJWA~%}PzW0(icL0#g@?06kCTJ&&p1V&OdYli2U+wgJ zs_P~JOwt@?@?s&H4pIa{Z%)lo zsOD5w<|mv7-QHjvUPG|9x%`)pu<`R#YJ*2#=$_>Z1Z@7PQLzm!zKEg^Wsl#jysxUF zH;AH_wE1u**p#-j^b=V7<0VgKZuIKZmhA~Ec-`H(QBO!cGw}mo7Mk6gWSZihnK2kU z4Hj=_x_;x}g%ULur;rEv^Fxendzu|tDHmic`pVP#=}fC$KdE&JuwIrQD#;9v?-5}X zS^hA%3W&GqdY<%@>k^>4b;9^zrz3@iHo8!voU=&D1CUG(Rpu8eqQ_g+r+AilT4>bf zZc%A8t(u7B6kDP!2A!p3pz_26xAav895kr?^49Sg*b^SP$%(+)<3%JlsRW&pmLjsKf+B1B=Zxd^D8Q=ueJUO{KHxs)I`L$`$+2iz~go&0)K)J;=#{ z>yba;E*GY4>vn*2F>hm}^sL}Ua{>~tRW8oW(NW*+YzQ1~a#Wl(5;8ygh1v57aNg3j z$fMRMeA|g~+}HwZuUtmdYqcBS3ivuzEXZv!Z(QiZY&u4)wPHWaTgC?s&(!S_iOOzvsI= zl&L$kE{e)!F(yT>|1v?=$kZBsEv8;|r)^ZuBjDCnQ@^p*$8Aa=8bj|v*ccT4%%1)J z$BY4|6kyk?|0}f@rvp2an1)duHo80@1kVF8XaMh~hjKJhFX+B>bY1}hB>=yC+w)@F?}(8KLz1JPa0^3m8N= zpVThh+ng(YnkNR0i0bAw@NOERVEvOesNAieq}xO`e+Cq~4%>09Si})v;{r5+WH*>y zt~(q_HIaO-%LK7G3?&c>Lq7qBSwW?t7p*_g~?OKlU#6(MQqOl;c_dJ0J85b-jPYX1#alf6E;9~yNXYQ1lx`U2Edg)E8w1nS%=hPZ(a z6@(EQZeG;vEx|i?s{3HD!V@<%U=@p-GiXW%k>WK8wSElJvKh{YVhgOV67SjKH)4yI zDkCSXNL7A3>80Iy@?hq+>$QnS&|wCVl_dzQklrvaxU`;~lYmM2W?8HB!W>BO0@;XW z;^#%#_|=N&AggD)Hm|mNjp~Cl#Za*a@acy0&&G6?>f}e{JibsBU`hr0s7#}e-LsDP z1jK=TkQ((&LYxJJf!al@&m!Q(+DI~VR~ooeX!m)+5;|!$+Cm@XF5T-I2q*2sdiZV^ zyN0d&%23TB+12db+L}%pbfu~&Cz-Zv;v*52Q>mK_CT}>2Su%i31@H8R?$q$z-qquW3T8uG!Kf$G|4;zBj|q*#se4y>4r$i)a~etMF6)fU zOU{N&&vFLuii-tg?$-Lrp@FnKWM^+1cyT+4bq@hQ06Xk_V!n-CX`I)gajYt$^d$Wh?TG*#cgKh;Sc;+fc(zsKQ1NEg0 zI(~vqr%fv)W9{4wA*|@5qU@`jlap*p!mnVdOv2pc5DD4bvu8Ujrv;0i6uhrA82ezH z7H7m2gZV|!urwi`rL|NwBplP_eOEbwwvFG4W0S+Iqmg)nfY>=94e1EN${$n}r%$nJT<>dCVP45L;S_W_lvD&zw_-Ho(tB^h*TvUo-ktz~baG zi?+Yj6nF3ni%kSvDgE;((T_~Dsqpp$CDPLBm42Z*gPG&ASHqPzh&UV?Twq9!U1`vd zr=|XxgR~^x2~e1zH^|BYo{dQD-fbaQeN?xc5^EbiPRdgS(DEAzljy~^1rZE3Bg5&i z9;5X$651m*Ox?p;X)lH)H!Vd9-u_n&4*=-VY#gFfk$0nwQ+G%V4JCkRF(3h=MCS!| z*nFhTmfwSj2;OP@2ddrCM}MByLh;H`V!T5zybwT~@t?8v0xB~*;=SP*1XZTC#(Kii z3#iTPjCY14NOge_AhbsNy=+wMHN>L7nLkrLE1`uU-o;##nwatrU9Q?%2g@zge-WDX z?pu*`f*j=Z7=nus2!hzTv4pBW=ZcC}&`KWnHFv)50@buD>7k_jbGQNn3POP)?Leu} zI>53Y20TkL3;zA=x|!d=O;CW3IeFGgI4f;FF{-al@c^NdfC7mxvv}Rd#lmOJ(KSvQ z-9~NMT6?GZBu7qfKU+)oPB8u!qEnQlxtcUy4AQlG^d(R=v(=Y+rJhPN)%v!edlp-j zx-A%5a3Xqv+@sIJ^N@iPG=ytcDsLR&Uvp?YHOgmECkUQ^v$iTw2ezdf6NqX^J0hjQ zUBB)izrW}!O8J+@zQd=WO8P|NInD;>UD7>^ zKm~dht1QkKy(w;Fe>y+ASKIyfp77u0hU$~XSA%VXoPY5>Zk|O`?i2xbih+ApNNp~u zfu>rWuF~Wt5k4qdn+2f@_>zDis3}z~T*nh1Pi(TN-g0lpvER|I4FS5s5EX1fUi%(A zR0LD)gM<{57{=!xEq~I?CRFeTa`P1IqwF_)j^{+*A+(Ck%}H*!8?@4~l(zX?G5K9J z^9cb#KmGR@Rq-+v$`CW`kh6}g;U}kzK>WF8K8@HJ!mcC{vJr39nGwm*DTr9Kp)~#E z9Hd$>l|w-pNDU5NHoYGb6|$k~CU$%WbkZHN#TkdiJ|wprM043Pf?4tb2S_Q}Q71(d+sdHz> zx|kv`W!j_{IQ(_n2Z~{>V_h?sMS86{Fn1M6xN*v<`b6~W*5gT1xPNDftk})gh?NbE zh@jwwoXXN*i$G^cV&!pmJ2jgZVr`VE!&Bww@^CoqHt|wYJK)7=t6uBc2k}I2;~*4d zubEIhiS+7ET4O$I>I#^heQ-=Ol$=`hdcDv<-zfycYh4YnpOnq~Lnw>1R=ABbx=^_y zn9fTevr5wueu$Al2SUAi6qI`u#@iJ;5up`xZquZB)k7_&&MhQn2AFmx73HuT4Zzmp z<|U+3WR+!Y(Q_mhy%|vC?I+yi%VUF@SPkC4`oCztvlmcNph;E@Uvk3);wj$EqcGt+ z_iG{cb%m%IwAodlF8O&rnGoO4DlX6qO;Iqz>^(RcBGmf9hQ`TBhjbXEhBR)NXXiP- zLO1YHU^*f0*`DDjAz2h%{jN_hgV$jc3!l$Yp%eS^lkK1@!EExb;M$!o9f@@>4z9QV z_y*`x5o-gYFn}z_l4%zdpFd<-p+CC5tBSt160f$H?$SYR<{}ObjRZ+yT<~}Ck!ZI{ zjUFPu97HrJ<>vNK=xEaP$a zS#a(L0u)3d4POSRk3UbvT=IWHAL$ZBh#q@K-!4Q!E4yWY!&5!{f;$o9LF=Yn|8|^| z#fv^nL+M*sppfV8RX8A~!&=Pu@Cg(=>US+Uy^#RfW|a~ z+UgqbG~8&s)OfG(UEn{(dyVz{-v|B$z(0a4eu%%>m_SYoZkP&{u}qLq`T+o<`W_dg zc8NKg2tG;0{mcGwiTQN0#{Hil@kROoPHgdK_h$nwxIf?xcZKg@wN+!!rVrv&WvqyB zesxRoM&MJBg89QSMeQc-pvVGscl8Z`UVX~UV8k~)8{~X( zSY7UOE<}(bcZX+_HjxcT!}3AwGn+j??Xt0UnA3p>dMjkc(H_kszHF^O5$X@yo@2i^ z>+JfL?Tdj#SH0}k-+4DqT?)FwV4E95{CgT#sJ&*}jgW-(=j`05NgB$3BT={b9o{D; z>Gd)S{aZL@SGub|WAVsI7}EF!5{XY4hBV(rikzbDcML}5eysBVie}(GSj+r(ho17C zv}Zs`iumJKf|h4klzL=C%KJSI1h8A@cW2ttnfJO}AMa{{G6L6=XRJCvd_s{hJCC<& z%n_(HO5o}!wjGd|+G_|=h`fAg4H7JkCOCU-mL>m=U3IG`WGMC3;90tW;KZ)WkKbEq zrmk#MJGSX>&3nzKwp|_c)2>E7hfV9Q9(-4-;k({5K}KWF-=XCG)pCy|5qP|@G+ap5 zZ3psUcF4dYrRL9}@ZLdf&-S=k?L6Nob~dFxrxY0Y#*XH1&9qOG#P7l-O#Cgl+&xxi znP}Syr8Js8{e<=AM6>dNTHrC-C{;E%s}CCJrTmv6E^|BZVGKGqw7q`c)ub1xREPm_ zrZYE=(y~W7aYV%wMikSNHeJYqQ2IOxDD&2K83@ zjz!s%^Iw%`D(nErhgU4meCrI`KHcQ!!{#>=Y%O=;>UCHQt%!kj-KL0>4ZeS(y!`Km zhT1Q`l)uZ@oDLC2_WxPn5Ol(5;}Da+R4#&(S`Oz-6Ok4?-UN&!eO1)UlXA}otw;CJ zEa0fft4`$qb~i>@#QUfrRaY9zd62e99h~9WG{CCg`KE5{$iP+iQD&Zw9NV+OQv)yc zA1oh;#4XPbp%n3F{Mve1fv4;{fN_BVl;3kWzWkm5F&`kC}u-=|@az9#t)%!tp)a0b4VVzv+t z8!Pbt2uKXK^C5TJh^0EJm(?p27sJ3Cf6~C|K(RKGHRMn8P*eNge+)lL?oJh+UxX+Z zQ6pP6=Cmt&3&KRpYfCr>bmedEKnYoXH0QJND7korYlI{OcQcfzL%yM4BoDV$52!rYwdzV*AaxwVN+p z^Dk?$=DtySSIO#8PZ;e%*HC)PMr}DnfZEyjA%Zl!H!Z9QPxhSz!M{Y!lPb()JnSlA z`Qj{LTpT%FX2Ag<(DnvH#pw_CI|H5D;R9I6cShrIwFS0n(_1(c(v6~OK4OD94%XAP zO!e?XEyO@j5BO09BRgh^Qy{HBm8ZtjjNjGy@NX`Cvr;P#i@jMO8dRr(vQ?Ok)F~k; zrMKf7jRdW}U zgRrcG#j7v?qDM6Cx^cg~z0u{DsX1Z8N-|QuJ&5{AqK~Y*2?_xeW=v>9S?Ng*7XMk# zk%Nc^Ju+a=-M_njICu)^R(R$&;cj+CVcid4%L-?1lnhf6PsQS}S5n4PS&kz>gaW1g z^7#wM3*-;71RrqZ?Ie+O-8jSlbIz>LF?2TS95RuxU z8ptqY5OSOtYhg2E$L(Yo{kRUizEU0&0oSggpj0pW;iYbe`eX!sSv5aVnCso`cA_O z7!Slr3HM$ugOkFyVjw0l5v+K?aDH?bybng6eGC=xth%oq#eKqU7z0ADda z&TO`f%jDjt;|WSY`y(?T)KUq8Bzhh3$}#-<_2bCyv-+FY<%JhOFt+UE)1{3ZTN+Gt z+VjUt?uUybw5cScZuQ~NBaLkJn1NHad6Wbq#)9o3dQY!24+|v!q5FY z#^mgH0>97}7qcACVN`b}dtqnL{&1P`dO(TadlChn+tR{LL4m^qS(!E+r@!2{?B3is zfKG2YwKC1^wlh2rA%+;G48L6R1*U+ENKNpNldv+=SV8OndW;H>n_mhzM412wH_OVk z95{KEVamBqAU>Q(CeE4`VBGBPpwUCYNdACKZ9w%y+oKtV#5d<&Zq%LcfV4!=wt_v&`y*&*j`_JM-)8puWqMlo@?II4Te z3VA*1;<^J*b96~=Yv*ayFL%q5>~D{RWkfpvPOrtfg>6$+W>E&-+V47K_pDmV5nf0H zvJV%Dpd;{Dus_GnC8o_x>?vZU!+ka$JP#1#^6X+YK8+Z0)KGqM!N-Hccou#BY@7| z5EG)pgxfNWtha-?EV(r1=xYHkJ4g>`JJUhqmad1{SJ1dz4K5J$%`%fHOdId`j}Pq# z3^h2IAI}pQq<92>@oW+s6)AppL9YYvr;Wg0m8%p!fARKDOD*G59y4B!Pclevf0#3O zd`SJ;&LZ}4<=AjN_wyLvyv!=q45y4oc0-&lZ$m}}FtvlIPAV>oszEm&UkC#Kxruyz z4h(ipl6FaW%mWfJ5YW7gm=d#V*OVbD?(<=@t*ufB+xh+*wKc`~2AFIuQ5*EOl87m_ z3FYx|-Az&lR_$&{kdA8=Ig!bmIHK&^%PH{1(TWkB(DTs~AU{7hDK7oN2vpzY9D~9)6=0aYhlBHL(FH> zzl;e5)#WM{;M#{-s@c_g|6?fq>7H8h4lm+}ls|_M;FE-oh9kTIZBl zIYt@r5K_e+Tc69lswu4U@^bJ5(=ERkIV;z?BBjg5rCMzkgjp?JRE!SEcc5T%2qO?Cf8nP^DV2UGvnxj`$xqeUWMQ!{Dy2t4KAtgZm4q_jF+J7tEX8) zajo(i)r@N-#I6oJ`o?etb-!(gwq_t{vzuTkT}R%12W~O5!j~QO2EEV1omc3vXpPWh zalZ|yQ-X(Lwt;I^frN5U1hzey0Qqk|kI9GB%&=8_aef-WZuTDh~*=tZ{LdPJXEaK9EpU_4x6 zqkUWW#iW&i3Zd>h)gtG8(X&!3hloXy!Dfltg(vmwSpLmgOFboYR+<{`v}Z7RPc$Nx zCpW0GKK`H}iqcUp%>^b=dq0VBKZ(fFTxxW1y=DR_u*?007JRZ#s_VUWH{UAuj4$dCw8z;_x+10u$rHGP%yp zOfBszA&k8lFNMkxPRggufx2K=FS5nlJ_ZXJkuZX7p+9Id7;ay=X^@YdTNK zqVNED%y2=Ea)K~V)1e)Ry1;(hB1oyU#O+ewU~1I+LI}Vy#P!>c1+77(kvO^;GmRj# zo@t<(W_m7;9it^k4MilL$J+kqt|a&KP?~1cfL_O8$Tt4@^KKwr?_+Q$o;nJ2LuVYoFsZU* zk1^`HjGPOf`~!)Zob1z-HiYGZiUw<~%O|c}Xx6?U(l9C?xJuy}v;X?pFHyG8_zb;s zOj9c?ooxKjW+zFs52`Zj&&0b#C?S|eNdO_vvTTy!bR$Ma?vnFy8Q7c5*DVyo1e@-g(ol{8ct z(Ee~S{QTQ8a#%sQBEUNsQQb@X7S63%pRM3~KTQ9SSZBT1iy>yw9lSZwj|wo#LWuMU zo-(K@bmSn%PrXTPyy9nZS@JlrO6!g=RE0`uU3i=a0$SOO_USQ5qaqQle5qU<$DG64 zapN#v<7#FnC+@qWy6PT#nb&R-6iA1S5<^qDht#3d;E}~>%OaP8h zS%xf2Xr}v3&AiZ?gc=+QY+v%Ysdl?i*Z;P}kl~ z5#2ORR2u*Q2Z+J$Blgui;4)J#p;tRovp@<#K_AC1-SfrR{FJohz@fzrq+P1B$Y%MJTyitMsGR0=^8_JBp0NJ#4VgLXD literal 0 HcmV?d00001 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/apps/docs/src/components/mdx/envelope-warning.tsx b/apps/docs/src/components/mdx/envelope-warning.tsx new file mode 100644 index 000000000..18676a78d --- /dev/null +++ b/apps/docs/src/components/mdx/envelope-warning.tsx @@ -0,0 +1,19 @@ +import { Callout } from 'fumadocs-ui/components/callout'; + +const MIGRATION_GUIDE_HREF = '/docs/developers/api/migrate-to-envelopes'; + +/** + * Deprecation banner steering API consumers away from the legacy document and + * template create endpoints and towards the unified Envelope API. + * + * Registered globally in `mdx-components.tsx`, so it can be used in any MDX page + * as `` without an explicit import. + */ +export function EnvelopeWarning() { + return ( + + Documents and templates are being deprecated and replaced by envelopes.{' '} + Read the migration guide here. + + ); +} diff --git a/apps/docs/src/mdx-components.tsx b/apps/docs/src/mdx-components.tsx index 298b70960..a0116880a 100644 --- a/apps/docs/src/mdx-components.tsx +++ b/apps/docs/src/mdx-components.tsx @@ -1,6 +1,7 @@ import * as TabsComponents from 'fumadocs-ui/components/tabs'; import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; +import { EnvelopeWarning } from '@/components/mdx/envelope-warning'; import { Mermaid } from '@/components/mdx/mermaid'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -9,6 +10,7 @@ export function getMDXComponents(components?: MDXComponents): any { ...defaultMdxComponents, ...TabsComponents, Mermaid, + EnvelopeWarning, ...components, }; } diff --git a/apps/openpage-api/package.json b/apps/openpage-api/package.json index bcc93e039..1b7c14350 100644 --- a/apps/openpage-api/package.json +++ b/apps/openpage-api/package.json @@ -12,11 +12,11 @@ "dependencies": { "@documenso/prisma": "*", "luxon": "^3.7.2", - "next": "16.2.6" + "next": "16.3.0" }, "devDependencies": { "@types/node": "^20", - "@types/react": "18.3.27", + "@types/react": "^19.2.17", "typescript": "5.6.2" } } 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/app/components/dialogs/branding-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx new file mode 100644 index 000000000..6205b1341 --- /dev/null +++ b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx @@ -0,0 +1,119 @@ +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Trans } from '@lingui/react/macro'; +import { useState } from 'react'; + +export type BrandingPreferencesResetDialogProps = { + hasAdvancedBranding: boolean; + isSubmitting: boolean; + onReset: () => Promise; + trigger?: React.ReactNode; +}; + +export const BrandingPreferencesResetDialog = ({ + hasAdvancedBranding, + isSubmitting, + onReset, + trigger, +}: BrandingPreferencesResetDialogProps) => { + const [open, setOpen] = useState(false); + const [isResetting, setIsResetting] = useState(false); + + const isLoading = isSubmitting || isResetting; + + const handleResetToDefaults = async () => { + setIsResetting(true); + + try { + await onReset(); + setOpen(false); + } catch { + // The submit handler surfaces its own error toast. Keep the dialog open + // so the user can retry. + } finally { + setIsResetting(false); + } + }; + + return ( + !isLoading && setOpen(value)}> + + {trigger ?? ( + + )} + + + + + + Reset branding preferences + + + + + This will reset all branding preferences to their default values and save the changes immediately. + + + + + + +

+ Once confirmed, the following will be reset: +

+ +
    +
  • + Custom branding enabled setting +
  • +
  • + Branding logo +
  • +
  • + Brand website and brand details +
  • +
  • + Brand colours, including background, foreground, primary, and border colours +
  • + + {hasAdvancedBranding && ( + <> +
  • + Border radius +
  • +
  • + Custom CSS +
  • + + )} +
+
+
+ + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/claim-update-dialog.tsx b/apps/remix/app/components/dialogs/claim-update-dialog.tsx index bcbd91a56..bdcdfbd55 100644 --- a/apps/remix/app/components/dialogs/claim-update-dialog.tsx +++ b/apps/remix/app/components/dialogs/claim-update-dialog.tsx @@ -2,6 +2,7 @@ import type { TLicenseClaim } from '@documenso/lib/types/license'; import { trpc } from '@documenso/trpc/react'; import type { TFindSubscriptionClaimsResponse } from '@documenso/trpc/server/admin-router/find-subscription-claims.types'; import { Button } from '@documenso/ui/primitives/button'; +import { Checkbox } from '@documenso/ui/primitives/checkbox'; import { Dialog, DialogContent, @@ -28,6 +29,7 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD const { toast } = useToast(); const [open, setOpen] = useState(false); + const [backportEmailTransport, setBackportEmailTransport] = useState(false); const { mutateAsync: updateClaim, isPending } = trpc.admin.claims.update.useMutation({ onSuccess: () => { @@ -67,19 +69,33 @@ export const ClaimUpdateDialog = ({ claim, trigger, licenseFlags }: ClaimUpdateD await updateClaim({ id: claim.id, data, + backportEmailTransport, }) } licenseFlags={licenseFlags} formSubmitTrigger={ - - + <> +
+ setBackportEmailTransport(checked === true)} + /> + +
- -
+ + + + + + } /> diff --git a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx deleted file mode 100644 index 694f2d56a..000000000 --- a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { FolderType } from '@documenso/lib/types/folder-type'; -import { formatDocumentsPath } from '@documenso/lib/utils/teams'; -import { trpc } from '@documenso/trpc/react'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -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'; -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 { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router'; -import { z } from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; - -export type DocumentMoveToFolderDialogProps = { - documentId: number; - open: boolean; - onOpenChange: (open: boolean) => void; - currentFolderId?: string; -} & Omit; - -const ZMoveDocumentFormSchema = z.object({ - folderId: z.string().nullable().optional(), -}); - -type TMoveDocumentFormSchema = z.infer; - -export const DocumentMoveToFolderDialog = ({ - documentId, - open, - onOpenChange, - currentFolderId, - ...props -}: DocumentMoveToFolderDialogProps) => { - const { _ } = useLingui(); - const { toast } = useToast(); - - const navigate = useNavigate(); - const team = useCurrentTeam(); - - const [searchTerm, setSearchTerm] = useState(''); - - const form = useForm({ - resolver: zodResolver(ZMoveDocumentFormSchema), - defaultValues: { - folderId: currentFolderId, - }, - }); - - const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery( - { - parentId: currentFolderId, - type: FolderType.DOCUMENT, - }, - { - enabled: open, - }, - ); - - const { mutateAsync: updateDocument } = trpc.document.update.useMutation(); - - useEffect(() => { - if (!open) { - form.reset(); - setSearchTerm(''); - } else { - form.reset({ folderId: currentFolderId }); - } - }, [open, currentFolderId, form]); - - const onSubmit = async (data: TMoveDocumentFormSchema) => { - try { - await updateDocument({ - documentId, - data: { - folderId: data.folderId ?? null, - }, - }); - - const documentsPath = formatDocumentsPath(team.url); - - if (data.folderId) { - await navigate(`${documentsPath}/f/${data.folderId}`); - } else { - await navigate(documentsPath); - } - - toast({ - title: _(msg`Document moved`), - description: _(msg`The document has been moved successfully.`), - variant: 'default', - }); - - onOpenChange(false); - } catch (err) { - const error = AppError.parseError(err); - - if (error.code === AppErrorCode.NOT_FOUND) { - toast({ - title: _(msg`Error`), - description: _(msg`The folder you are trying to move the document to does not exist.`), - variant: 'destructive', - }); - - return; - } - - if (error.code === AppErrorCode.UNAUTHORIZED) { - toast({ - title: _(msg`Error`), - description: _(msg`You are not allowed to move this document.`), - variant: 'destructive', - }); - - return; - } - - toast({ - title: _(msg`Error`), - description: _(msg`An error occurred while moving the document.`), - variant: 'destructive', - }); - } - }; - - const filteredFolders = folders?.data.filter((folder) => - folder.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - return ( - - - - - Move Document to Folder - - - - Select a folder to move this document to. - - - -
- - setSearchTerm(e.target.value)} - className="pl-8" - /> -
- -
- - ( - - - Folder - - - -
- {isFoldersLoading ? ( -
- -
- ) : ( - <> - - - {filteredFolders?.map((folder) => ( - - ))} - - {searchTerm && filteredFolders?.length === 0 && ( -
- No folders found -
- )} - - )} -
-
- -
- )} - /> - - - - - - - - -
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx new file mode 100644 index 000000000..ed7184ef7 --- /dev/null +++ b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx @@ -0,0 +1,122 @@ +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Trans } from '@lingui/react/macro'; +import { useState } from 'react'; + +export type DocumentPreferencesResetDialogProps = { + isSubmitting: boolean; + onReset: () => Promise; + showAiFeatures?: boolean; + showDocumentVisibility?: boolean; +}; + +export const DocumentPreferencesResetDialog = ({ + isSubmitting, + onReset, + showAiFeatures = false, + showDocumentVisibility = false, +}: DocumentPreferencesResetDialogProps) => { + const [open, setOpen] = useState(false); + const [isResetting, setIsResetting] = useState(false); + + const isLoading = isSubmitting || isResetting; + + const handleResetToDefaults = async () => { + setIsResetting(true); + + try { + await onReset(); + setOpen(false); + } catch { + // The submit handler surfaces its own error toast. Keep the dialog open + // so the user can retry. + } finally { + setIsResetting(false); + } + }; + + return ( + !isLoading && setOpen(value)}> + + + + + + + + Reset document preferences + + + + + This will reset all document preferences to their default values and save the changes immediately. + + + + + + +

+ Once confirmed, the following will be reset: +

+ +
    + {showDocumentVisibility && ( +
  • + Default document visibility +
  • + )} +
  • + Default document language +
  • +
  • + Default date format +
  • +
  • + Default time zone +
  • +
  • + Default signature settings +
  • +
  • + Default recipients +
  • +
  • + Delegate document ownership +
  • + {showAiFeatures && ( +
  • + AI features +
  • + )} +
+
+
+ + + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/document-resend-dialog.tsx b/apps/remix/app/components/dialogs/document-resend-dialog.tsx deleted file mode 100644 index d4e4a5168..000000000 --- a/apps/remix/app/components/dialogs/document-resend-dialog.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { useSession } from '@documenso/lib/client-only/providers/session'; -import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; -import type { TRecipientLite } from '@documenso/lib/types/recipient'; -import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; -import type { Document } from '@documenso/prisma/types/document-legacy-schema'; -import { trpc as trpcReact } from '@documenso/trpc/react'; -import { cn } from '@documenso/ui/lib/utils'; -import { Button } from '@documenso/ui/primitives/button'; -import { Checkbox } from '@documenso/ui/primitives/checkbox'; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@documenso/ui/primitives/dialog'; -import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu'; -import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form'; -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 { SigningStatus, type Team, type User } from '@prisma/client'; -import { History } from 'lucide-react'; -import { useState } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; -import * as z from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; - -import { StackAvatar } from '../general/stack-avatar'; - -const FORM_ID = 'resend-email'; - -export type DocumentResendDialogProps = { - document: Pick & { - user: Pick; - recipients: TRecipientLite[]; - team: Pick | null; - }; - recipients: TRecipientLite[]; -}; - -export const ZResendDocumentFormSchema = z.object({ - recipients: z.array(z.number()).min(1, { - message: 'You must select at least one item.', - }), -}); - -export type TResendDocumentFormSchema = z.infer; - -export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => { - const { user } = useSession(); - const team = useCurrentTeam(); - - const { toast } = useToast(); - const { _ } = useLingui(); - - const [isOpen, setIsOpen] = useState(false); - const isOwner = document.userId === user.id; - const isCurrentTeamDocument = team && document.team?.url === team.url; - - const isDisabled = - (!isOwner && !isCurrentTeamDocument) || - document.status !== 'PENDING' || - !recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED); - - const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation(); - - const form = useForm({ - resolver: zodResolver(ZResendDocumentFormSchema), - defaultValues: { - recipients: [], - }, - }); - - const { - handleSubmit, - formState: { isSubmitting }, - } = form; - - const selectedRecipients = useWatch({ - control: form.control, - name: 'recipients', - }); - - const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => { - try { - await resendDocument({ documentId: document.id, recipients }); - - toast({ - title: _(msg`Document re-sent`), - description: _(msg`Your document has been re-sent successfully.`), - duration: 5000, - }); - - setIsOpen(false); - } catch (err) { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`This document could not be re-sent at this time. Please try again.`), - variant: 'destructive', - duration: 7500, - }); - } - }; - - return ( - - - e.preventDefault()}> - - Resend - - - - - - -

- Who do you want to remind? -

-
-
- -
- - ( - <> - {recipients.map((recipient) => ( - - - - {recipient.email} - - - - - checked - ? onChange([...value, recipient.id]) - : onChange(value.filter((v) => v !== recipient.id)) - } - /> - - - ))} - - )} - /> - - - - -
- - - - - -
-
-
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/email-transport-create-dialog.tsx b/apps/remix/app/components/dialogs/email-transport-create-dialog.tsx new file mode 100644 index 000000000..462e41921 --- /dev/null +++ b/apps/remix/app/components/dialogs/email-transport-create-dialog.tsx @@ -0,0 +1,95 @@ +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; + +import { + EmailTransportForm, + type EmailTransportFormValues, + emailTransportFormToConfig, +} from '../forms/email-transport-form'; + +export type EmailTransportCreateDialogProps = { + trigger?: React.ReactNode; +}; + +export const EmailTransportCreateDialog = ({ trigger }: EmailTransportCreateDialogProps) => { + const { t } = useLingui(); + const { toast } = useToast(); + + const [open, setOpen] = useState(false); + + const { mutateAsync: createTransport, isPending } = trpc.admin.emailTransport.create.useMutation({ + onSuccess: () => { + toast({ + title: t`Transport created.`, + }); + + setOpen(false); + }, + onError: (error) => { + toast({ + title: t`Failed to create transport.`, + description: error.message, + variant: 'destructive', + }); + }, + }); + + const onFormSubmit = async (values: EmailTransportFormValues) => { + await createTransport({ + name: values.name, + fromName: values.fromName, + fromAddress: values.fromAddress, + config: emailTransportFormToConfig(values), + }); + }; + + return ( + !isPending && setOpen(value)}> + e.stopPropagation()} asChild> + {trigger ?? ( + + )} + + + + + + Add Email Transport + + + Fill in the details to create a new email transport. + + + + + + + + + } + /> + + + ); +}; diff --git a/apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx b/apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx new file mode 100644 index 000000000..055d920b3 --- /dev/null +++ b/apps/remix/app/components/dialogs/email-transport-delete-dialog.tsx @@ -0,0 +1,114 @@ +import { trpc } from '@documenso/trpc/react'; +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Plural, Trans, useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; + +export type EmailTransportDeleteDialogProps = { + transportId: string; + transportName: string; + subscriptionClaimCount: number; + organisationClaimCount: number; + trigger: React.ReactNode; +}; + +export const EmailTransportDeleteDialog = ({ + transportId, + transportName, + subscriptionClaimCount, + organisationClaimCount, + trigger, +}: EmailTransportDeleteDialogProps) => { + const { t } = useLingui(); + const { toast } = useToast(); + + const [open, setOpen] = useState(false); + + const isInUse = subscriptionClaimCount + organisationClaimCount > 0; + + const { mutateAsync: deleteTransport, isPending } = trpc.admin.emailTransport.delete.useMutation({ + onSuccess: () => { + toast({ + title: t`Transport deleted.`, + }); + + setOpen(false); + }, + onError: () => { + toast({ + title: t`Failed to delete transport.`, + variant: 'destructive', + }); + }, + }); + + return ( + !isPending && setOpen(value)}> + e.stopPropagation()}> + {trigger} + + + + + + Delete Email Transport + + + Are you sure you want to delete the following transport? + + + + + {transportName} + + + {isInUse && ( + + + Warning, this email transport is currently being used by: + +
    + {subscriptionClaimCount > 0 && ( +
  • + +
  • + )} + + {organisationClaimCount > 0 && ( +
  • + +
  • + )} +
+
+
+ )} + + + + + + +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx b/apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx new file mode 100644 index 000000000..1a463ff72 --- /dev/null +++ b/apps/remix/app/components/dialogs/email-transport-send-test-dialog.tsx @@ -0,0 +1,126 @@ +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +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'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +const ZSendTestEmailFormSchema = z.object({ + to: z.string().email(), +}); + +type TSendTestEmailFormSchema = z.infer; + +export type EmailTransportSendTestDialogProps = { + transportId: string; + trigger: React.ReactNode; +}; + +export const EmailTransportSendTestDialog = ({ transportId, trigger }: EmailTransportSendTestDialogProps) => { + const { t } = useLingui(); + const { toast } = useToast(); + + const [open, setOpen] = useState(false); + + const { mutateAsync: sendTest } = trpc.admin.emailTransport.sendTest.useMutation({ + onSuccess: () => { + toast({ + title: t`Test email sent.`, + }); + setOpen(false); + }, + onError: (error) => { + toast({ + title: t`Test failed.`, + description: error.message, + variant: 'destructive', + }); + }, + }); + + const form = useForm({ + resolver: zodResolver(ZSendTestEmailFormSchema), + defaultValues: { + to: '', + }, + }); + + const onFormSubmit = async ({ to }: TSendTestEmailFormSchema) => { + await sendTest({ id: transportId, to }); + }; + + useEffect(() => { + if (!open) { + form.reset(); + } + }, [open, form]); + + return ( + !form.formState.isSubmitting && setOpen(value)}> + e.stopPropagation()}> + {trigger} + + + + + + Send Test Email + + + Send a test email using this transport to verify the configuration. + + + +
+ +
+ ( + + + Email + + + + + + + )} + /> + + + + + + +
+
+ +
+
+ ); +}; diff --git a/apps/remix/app/components/dialogs/email-transport-update-dialog.tsx b/apps/remix/app/components/dialogs/email-transport-update-dialog.tsx new file mode 100644 index 000000000..5ad3ae023 --- /dev/null +++ b/apps/remix/app/components/dialogs/email-transport-update-dialog.tsx @@ -0,0 +1,104 @@ +import { trpc } from '@documenso/trpc/react'; +import type { TFindEmailTransportsResponse } from '@documenso/trpc/server/admin-router/email-transport/find-email-transports.types'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useState } from 'react'; + +import { + EmailTransportForm, + type EmailTransportFormValues, + emailTransportFormToConfig, +} from '../forms/email-transport-form'; + +export type EmailTransportUpdateDialogProps = { + transport: TFindEmailTransportsResponse['data'][number]; + trigger: React.ReactNode; +}; + +export const EmailTransportUpdateDialog = ({ transport, trigger }: EmailTransportUpdateDialogProps) => { + const { t } = useLingui(); + const { toast } = useToast(); + + const [open, setOpen] = useState(false); + + const { mutateAsync: updateTransport, isPending } = trpc.admin.emailTransport.update.useMutation(); + + const onFormSubmit = async (values: EmailTransportFormValues) => { + try { + await updateTransport({ + id: transport.id, + data: { + name: values.name, + fromName: values.fromName, + fromAddress: values.fromAddress, + config: emailTransportFormToConfig(values), + }, + }); + + toast({ + title: t`Transport updated.`, + }); + + setOpen(false); + } catch { + toast({ + title: t`Failed to save transport.`, + variant: 'destructive', + }); + } + }; + + return ( + !isPending && setOpen(value)}> + e.stopPropagation()}> + {trigger} + + + + + + Edit Email Transport + + + Modify the details of the email transport. + + + + + + + + + } + /> + + + ); +}; diff --git a/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx new file mode 100644 index 000000000..8cb90cfa0 --- /dev/null +++ b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx @@ -0,0 +1,134 @@ +import { trpc as trpcReact } from '@documenso/trpc/react'; +import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Label } from '@documenso/ui/primitives/label'; +import { Textarea } from '@documenso/ui/primitives/textarea'; +import { useToast } from '@documenso/ui/primitives/use-toast'; +import { Trans, useLingui } from '@lingui/react/macro'; +import { useEffect, useState } from 'react'; + +export type EnvelopeCancelDialogProps = { + id: string; + title: string; + trigger?: React.ReactNode; + onCancel?: () => Promise | void; +}; + +export const EnvelopeCancelDialog = ({ id, title, trigger, onCancel }: EnvelopeCancelDialogProps) => { + const { toast } = useToast(); + const { t } = useLingui(); + const trpcUtils = trpcReact.useUtils(); + + const [open, setOpen] = useState(false); + const [reason, setReason] = useState(''); + + const { mutateAsync: cancelEnvelope, isPending } = trpcReact.envelope.cancel.useMutation({ + onSuccess: async () => { + toast({ + title: t`Document cancelled`, + description: t`"${title}" has been successfully cancelled`, + duration: 5000, + }); + + await trpcUtils.document.findDocumentsInternal.invalidate(); + + await onCancel?.(); + + setOpen(false); + }, + onError: () => { + toast({ + title: t`Something went wrong`, + description: t`This document could not be cancelled at this time. Please try again.`, + variant: 'destructive', + duration: 7500, + }); + }, + }); + + useEffect(() => { + if (open) { + setReason(''); + } + }, [open]); + + return ( + !isPending && setOpen(value)}> + {trigger} + + + + + Are you sure? + + + + + You are about to cancel "{title}" + + + + + + +

+ Once confirmed, the following will occur: +

+ +
    +
  • + The document signing process will be stopped +
  • +
  • + Recipients will be notified that the document was cancelled +
  • +
  • + The document will remain in your dashboard marked as Cancelled +
  • +
+
+
+ +
+ + +