Merge branch 'main' into feature/pdf-placeholder-selection-fields

This commit is contained in:
Catalin Pit
2026-05-26 08:54:20 +03:00
committed by GitHub
71 changed files with 3245 additions and 1473 deletions
@@ -12,7 +12,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
| 21 CFR Part 11 | Compliant (Enterprise) |
| SOC 2 | Compliant |
| ISO 27001 | Planned |
| HIPAA | Planned |
| HIPAA | Compliant (Enterprise) |
## 21 CFR Part 11
@@ -0,0 +1,45 @@
---
title: Common Errors
description: A comprehensive troubleshooting matrix for Documenso API and Webhook integration errors.
---
This guide provides a comprehensive troubleshooting matrix for the standard error codes returned by the Documenso API. Use this reference to diagnose and resolve integration issues related to envelopes, recipients, and webhooks.
## Application Error Codes
| Error Code | Description | Recommended Action |
| :--- | :--- | :--- |
| `ALREADY_EXISTS` | The resource you are attempting to create already exists. | Verify if the entity (e.g., user, envelope, webhook) has already been instantiated. Use a `PUT` or `PATCH` request to update the existing resource instead of `POST`. |
| `EXPIRED_CODE` | The provided access code or token has expired. | Generate a new access code or request a new invitation link before retrying the request. |
| `INVALID_BODY` | The request payload is malformed. | Inspect your JSON payload structure. Ensure it strictly adheres to the expected schema and that no required fields are missing. |
| `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. |
| `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. |
| `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. |
| `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. |
| `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. |
| `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. |
| `INVALID_CAPTCHA` | Security token (Captcha) validation failed. | Ensure the Captcha token is correctly generated on the client side and transmitted without alteration in your request. |
| `UNAUTHORIZED` | Missing or invalid authentication (401). | Verify that your API key is correct, active, and properly formatted in the `Authorization` header (e.g., `Bearer <YOUR_API_KEY>`). |
| `FORBIDDEN` | Access to the resource is denied (403). | Ensure your API key or user account has the necessary permissions and roles to execute this specific action. |
| `UNKNOWN_ERROR` | An unexpected internal server error occurred (500). | Retry the request later. If the issue persists, contact technical support with your request payload and the timestamp of the incident. |
| `RETRY_EXCEPTION` | The operation failed temporarily but can be retried. | Implement an automatic retry logic in your integration, ideally using an exponential backoff strategy. |
| `SCHEMA_FAILED` | Strict data schema validation failed. | Verify that the data types sent (string, number, boolean) exactly match the OpenAPI specification. |
| `TOO_MANY_REQUESTS` | Rate limit exceeded (429). | Reduce the frequency of your API calls. Implement rate-limiting handling based on the response headers. |
| `TWO_FACTOR_AUTH_FAILED` | Two-factor authentication (2FA) failed. | Verify the provided 2FA code. Ensure it was entered correctly and has not expired. |
| `WEBHOOK_INVALID_REQUEST` | The webhook-related request is invalid. | Check your receiving endpoint configuration. Ensure the URL is correct and that your server accepts `POST` requests from Documenso. |
## Envelope State Errors
The following errors occur when attempting to perform actions on an envelope that are incompatible with its current state.
| Error Code | Description | Recommended Action |
| :--- | :--- | :--- |
| `ENVELOPE_DRAFT` | The action cannot be performed because the envelope is still in a draft state. | Finalize the envelope configuration and transition it to the `PENDING` (sent) state before attempting this operation. |
| `ENVELOPE_COMPLETED` | The action cannot be performed because the envelope is already completed. | No further modifications (e.g., adding signers, modifying documents) can be made to an envelope once the signing process is finished. |
| `ENVELOPE_REJECTED` | The action cannot be performed because the envelope was rejected by a recipient. | The signing flow is permanently halted. Create a new envelope if you wish to resubmit the document. |
| `ENVELOPE_LEGACY` | The action cannot be performed because the envelope uses an obsolete format. | This envelope was created with a legacy version of the system. Recreate the envelope using the current API version to interact with it. |
## See Also
- [Documents API](/docs/developers/api/documents)
- [Webhooks](/docs/developers/webhooks)
@@ -1,4 +1,14 @@
{
"title": "API Reference",
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
"pages": [
"documents",
"recipients",
"fields",
"templates",
"teams",
"rate-limits",
"versioning",
"developer-mode",
"common-errors"
]
}
@@ -73,14 +73,14 @@ Include the token in the `Authorization` header of your HTTP requests.
### cURL
```bash
curl https://app.documenso.com/api/v2/documents \
curl https://app.documenso.com/api/v2/document \
-H "Authorization: api_xxxxxxxxxxxxxxxx"
```
### JavaScript / TypeScript
```typescript
const response = await fetch('https://app.documenso.com/api/v2/documents', {
const response = await fetch('https://app.documenso.com/api/v2/document', {
method: 'GET',
headers: {
Authorization: 'api_xxxxxxxxxxxxxxxx',
+2 -2
View File
@@ -16,7 +16,7 @@
"fumadocs-ui": "16.5.0",
"lucide-react": "^0.563.0",
"mermaid": "^11.12.2",
"next": "16.2.4",
"next": "16.2.6",
"next-plausible": "^3.12.5",
"next-themes": "^0.4.6",
"react": "^19.2.4",
@@ -29,7 +29,7 @@
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"postcss": "^8.5.14",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
+1 -1
View File
@@ -12,7 +12,7 @@
"dependencies": {
"@documenso/prisma": "*",
"luxon": "^3.7.2",
"next": "16.2.4"
"next": "16.2.6"
},
"devDependencies": {
"@types/node": "^20",
@@ -0,0 +1,152 @@
import { AppError } from '@documenso/lib/errors/app-error';
import { trpc } from '@documenso/trpc/react';
import { ZCreateUserRequestSchema } from '@documenso/trpc/server/admin-router/create-user.types';
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 type * as DialogPrimitive from '@radix-ui/react-dialog';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import type { z } from 'zod';
export type AdminUserCreateDialogProps = {
trigger?: React.ReactNode;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
const ZFormSchema = ZCreateUserRequestSchema;
type TFormSchema = z.infer<typeof ZFormSchema>;
export const AdminUserCreateDialog = ({ trigger, ...props }: AdminUserCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const form = useForm<TFormSchema>({
resolver: zodResolver(ZFormSchema),
defaultValues: {
email: '',
name: '',
},
});
const { mutateAsync: createUser } = trpc.admin.user.create.useMutation();
const onFormSubmit = async (data: TFormSchema) => {
try {
const result = await createUser(data);
await navigate(`/admin/users/${result.userId}`);
setOpen(false);
toast({
title: t`Success`,
description: t`User created and welcome email sent`,
duration: 5000,
});
} catch (err) {
const error = AppError.parseError(err);
console.error(error);
toast({
title: t`An error occurred`,
description: error.message || t`We encountered an error while creating the user. Please try again later.`,
variant: 'destructive',
});
}
};
useEffect(() => {
form.reset();
}, [open, form]);
return (
<Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
{trigger ?? (
<Button className="flex-shrink-0" variant="secondary">
<Trans>Create User</Trans>
</Button>
)}
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Create User</Trans>
</DialogTitle>
<DialogDescription>
<Trans>Create a new user. A welcome email will be sent with a link to set their password.</Trans>
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
<FormControl>
<Input {...field} type="email" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel required>
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
<Trans>Cancel</Trans>
</Button>
<Button type="submit" data-testid="dialog-create-user-button" loading={form.formState.isSubmitting}>
<Trans>Create</Trans>
</Button>
</DialogFooter>
</fieldset>
</form>
</Form>
</DialogContent>
</Dialog>
);
};
@@ -116,11 +116,12 @@ export const EditorFieldDropdownForm = ({
}
const newValues = [...currentValues];
const removedValue = currentValues[index].value;
newValues.splice(index, 1);
form.setValue('values', newValues);
if (form.getValues('defaultValue') === newValues[index].value) {
if (form.getValues('defaultValue') === removedValue) {
form.setValue('defaultValue', undefined);
}
};
+23 -15
View File
@@ -89,7 +89,6 @@ export const SignInForm = ({
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const twoFactorTurnstileRef = useRef<TurnstileInstance>(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
@@ -197,13 +196,31 @@ export const SignInForm = ({
};
const onFormSubmit = async ({ email, password, totpCode, backupCode }: TSignInFormSchema) => {
const $turnstile = isTwoFactorAuthenticationDialogOpen ? twoFactorTurnstileRef.current : turnstileRef.current;
try {
let token: string | undefined;
if (turnstileSiteKey) {
token = await $turnstile?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signIn({
email,
password,
totpCode,
backupCode,
captchaToken: captchaToken ?? undefined,
captchaToken: token ?? undefined,
redirectPath,
});
} catch (err) {
@@ -214,10 +231,6 @@ export const SignInForm = ({
if (error.code === 'TWO_FACTOR_MISSING_CREDENTIALS') {
setIsTwoFactorAuthenticationDialogOpen(true);
// Turnstile tokens are single-use. Clear the consumed one so the
// dialog's fresh widget mounts cleanly and the dialog can't be
// submitted with the stale token before a new one is issued.
setCaptchaToken(null);
return;
}
@@ -247,8 +260,7 @@ export const SignInForm = ({
variant: 'destructive',
});
turnstileRef.current?.reset();
setCaptchaToken(null);
$turnstile?.reset();
}
};
@@ -358,11 +370,9 @@ export const SignInForm = ({
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
)}
@@ -499,11 +509,9 @@ export const SignInForm = ({
<Turnstile
ref={twoFactorTurnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
</div>
@@ -518,7 +526,7 @@ export const SignInForm = ({
)}
</Button>
<Button type="submit" loading={isSubmitting} disabled={Boolean(turnstileSiteKey) && !captchaToken}>
<Button type="submit" loading={isSubmitting}>
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
</Button>
</DialogFooter>
+19 -8
View File
@@ -20,7 +20,7 @@ import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
@@ -86,8 +86,6 @@ export const SignUpForm = ({
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const hasSocialAuthEnabled = isGoogleSignupEnabled || isMicrosoftSignupEnabled || isOidcSignupEnabled;
const form = useForm<TSignUpFormSchema>({
@@ -105,12 +103,28 @@ export const SignUpForm = ({
const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => {
try {
let token: string | undefined;
if (turnstileSiteKey) {
token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signUp({
name,
email,
password,
signature,
captchaToken: captchaToken ?? undefined,
captchaToken: token ?? undefined,
});
await navigate(returnTo ? returnTo : '/unverified-account');
@@ -140,7 +154,6 @@ export const SignUpForm = ({
});
turnstileRef.current?.reset();
setCaptchaToken(null);
}
};
@@ -316,11 +329,9 @@ export const SignUpForm = ({
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
onSuccess={setCaptchaToken}
onExpire={() => setCaptchaToken(null)}
options={{
size: 'flexible',
appearance: 'interaction-only',
appearance: 'always',
}}
/>
)}
@@ -1,6 +1,7 @@
import { authClient } from '@documenso/auth/client';
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
import { AppError } from '@documenso/lib/errors/app-error';
import { env } from '@documenso/lib/utils/env';
import { zEmail } from '@documenso/lib/utils/zod';
import { ZPasswordSchema } from '@documenso/trpc/server/auth-router/schema';
import { Button } from '@documenso/ui/primitives/button';
@@ -12,6 +13,9 @@ 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 { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import { useRef } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { z } from 'zod';
@@ -50,6 +54,9 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const analytics = useAnalytics();
const navigate = useNavigate();
const turnstileSiteKey = env('NEXT_PUBLIC_TURNSTILE_SITE_KEY');
const turnstileRef = useRef<TurnstileInstance>(null);
const form = useForm<TClaimAccountFormSchema>({
values: {
name: defaultName ?? '',
@@ -61,7 +68,28 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
const onFormSubmit = async ({ name, email, password }: TClaimAccountFormSchema) => {
try {
await authClient.emailPassword.signUp({ name, email, password });
let token: string | undefined;
if (turnstileSiteKey) {
token = await turnstileRef.current?.getResponsePromise(3000).catch((_err) => undefined);
if (!token) {
toast({
title: _(msg`Human verification required`),
description: _(msg`Please complete the CAPTCHA challenge before signing in.`),
variant: 'destructive',
});
return;
}
}
await authClient.emailPassword.signUp({
name,
email,
password,
captchaToken: token ?? undefined,
});
await navigate(`/unverified-account`);
@@ -87,6 +115,8 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
description: _(errorMessage),
variant: 'destructive',
});
turnstileRef.current?.reset();
}
};
@@ -141,6 +171,19 @@ export const ClaimAccount = ({ defaultName, defaultEmail }: ClaimAccountProps) =
)}
/>
{turnstileSiteKey && (
<div className="mt-4">
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
options={{
size: 'flexible',
appearance: 'always',
}}
/>
</div>
)}
<Button type="submit" className="mt-6 w-full" loading={form.formState.isSubmitting}>
<Trans>Claim account</Trans>
</Button>
@@ -97,7 +97,7 @@ export const DocumentSigningMobileWidget = () => {
layoutId="document-signing-mobile-widget-progress-bar"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
width: `${requiredRecipientFields.length === 0 ? 100 : 100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
/>
</div>
@@ -142,7 +142,7 @@ export const DocumentSigningPageViewV1 = ({
return undefined;
}
const sortedRecipients = allRecipients.sort((a, b) => {
const sortedRecipients = [...allRecipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
@@ -150,7 +150,7 @@ export const DocumentSigningPageViewV2 = () => {
layoutId="document-flow-container-step"
className="absolute inset-y-0 left-0 bg-primary"
style={{
width: `${100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
width: `${requiredRecipientFields.length === 0 ? 100 : 100 - (100 / requiredRecipientFields.length) * (recipientFieldsRemaining.length ?? 0)}%`,
}}
/>
</div>
@@ -294,7 +294,7 @@ export const EnvelopeSigningProvider = ({
return null;
}
const sortedRecipients = envelope.recipients.sort((a, b) => {
const sortedRecipients = [...envelope.recipients].sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) {
return a.id - b.id;
@@ -334,8 +334,6 @@ export const EnvelopeSignerPageRenderer = ({ pageData }: { pageData: PageRenderD
fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload);
}
loadingSpinnerGroup.destroy();
})
.finally(() => {
loadingSpinnerGroup.destroy();
@@ -25,7 +25,7 @@ export const CardMetric = ({ icon: Icon, title, value, className, children }: Ca
</div>
)}
<h3 className="mb-2 flex items-end font-medium text-primary-forground text-sm leading-tight">{title}</h3>
<h3 className="mb-2 flex items-end font-medium text-sm leading-tight">{title}</h3>
</div>
{children || (
@@ -238,7 +238,7 @@ export const AdminOrganisationsTable = ({
}}
>
{(table) =>
!hidePaginationUntilOverflow || 1 > table.getPageCount() ? (
!hidePaginationUntilOverflow || table.getPageCount() > 1 ? (
<DataTablePagination additionalInformation="VisibleCount" table={table} />
) : null
}
@@ -1,6 +1,7 @@
import { findUsers } from '@documenso/lib/server-only/user/get-all-users';
import { Trans } from '@lingui/react/macro';
import { AdminUserCreateDialog } from '~/components/dialogs/admin-user-create-dialog';
import { AdminDashboardUsersTable } from '~/components/tables/admin-dashboard-users-table';
import type { Route } from './+types/users._index';
@@ -27,9 +28,13 @@ export default function AdminManageUsersPage({ loaderData }: Route.ComponentProp
return (
<div>
<h2 className="font-semibold text-4xl">
<Trans>Manage users</Trans>
</h2>
<div className="mb-6 flex items-center justify-between">
<h2 className="font-semibold text-4xl">
<Trans>Manage users</Trans>
</h2>
<AdminUserCreateDialog />
</div>
<AdminDashboardUsersTable users={users} totalPages={totalPages} page={page} perPage={perPage} />
</div>
@@ -40,6 +40,6 @@ export const handleInitialsFieldClick = async (
return {
type: FieldType.INITIALS,
value: initials,
value: initialsToInsert,
};
};