From a294f97e6eca11ed601057759253bd5c5cb36124 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:06 -0300 Subject: [PATCH 01/12] fix(contract): allow optional nullable signer email in response definitions Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/ResponseDefinitions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 096b026be3..6d46e07610 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -142,7 +142,7 @@ * @psalm-type LibresignSignerSummary = array{ * signRequestId: int, * displayName: string, - * email: string, + * email?: ?string, * identifyMethods?: LibresignIdentifyMethod[], * signed: ?string, * status: int, @@ -394,7 +394,7 @@ * statusText: string, * nodeId: non-negative-int, * nodeType: 'file'|'envelope', - * signatureFlow: int, + * signatureFlow: 0|1|2, * docmdpLevel: int, * filesCount: int<0, max>, * files: list, From c317cce6deb9f9ffe18fb72c2ea8c8d05d9d5190 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:06 -0300 Subject: [PATCH 02/12] chore(openapi): regenerate default spec for signer email contract Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openapi.json b/openapi.json index 0205a793e2..0c8286187b 100644 --- a/openapi.json +++ b/openapi.json @@ -1839,7 +1839,6 @@ "required": [ "signRequestId", "displayName", - "email", "signed", "status", "statusText" @@ -1853,7 +1852,8 @@ "type": "string" }, "email": { - "type": "string" + "type": "string", + "nullable": true }, "identifyMethods": { "type": "array", @@ -2156,7 +2156,12 @@ }, "signatureFlow": { "type": "integer", - "format": "int64" + "format": "int64", + "enum": [ + 0, + 1, + 2 + ] }, "docmdpLevel": { "type": "integer", From 097beb4b8f6a3cb462ba5e1393354590523150fe Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 03/12] chore(openapi): regenerate full spec for signer email contract Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-full.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openapi-full.json b/openapi-full.json index 9ab790941c..d56503b4fb 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -2437,7 +2437,6 @@ "required": [ "signRequestId", "displayName", - "email", "signed", "status", "statusText" @@ -2451,7 +2450,8 @@ "type": "string" }, "email": { - "type": "string" + "type": "string", + "nullable": true }, "identifyMethods": { "type": "array", @@ -2768,7 +2768,12 @@ }, "signatureFlow": { "type": "integer", - "format": "int64" + "format": "int64", + "enum": [ + 0, + 1, + 2 + ] }, "docmdpLevel": { "type": "integer", From 88dc356aeca059d1daca448ffe87be443423b271 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 04/12] chore(types): regenerate default openapi types for signer email contract Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 05c59f901e..92dd34df6b 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1565,7 +1565,7 @@ export type components = { /** Format: int64 */ signRequestId: number; displayName: string; - email: string; + email?: string | null; identifyMethods?: components["schemas"]["IdentifyMethod"][]; signed: string | null; /** Format: int64 */ @@ -1653,8 +1653,11 @@ export type components = { nodeId: number; /** @enum {string} */ nodeType: "file" | "envelope"; - /** Format: int64 */ - signatureFlow: number; + /** + * Format: int64 + * @enum {integer} + */ + signatureFlow: 0 | 1 | 2; /** Format: int64 */ docmdpLevel: number; /** Format: int64 */ From 320729c3720f827b19ae69de2e209c4e1f950c43 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 05/12] chore(types): regenerate full openapi types for signer email contract Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/types/openapi/openapi-full.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index cb21c1a4b1..2bc6b201c6 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -2210,7 +2210,7 @@ export type components = { /** Format: int64 */ signRequestId: number; displayName: string; - email: string; + email?: string | null; identifyMethods?: components["schemas"]["IdentifyMethod"][]; signed: string | null; /** Format: int64 */ @@ -2302,8 +2302,11 @@ export type components = { nodeId: number; /** @enum {string} */ nodeType: "file" | "envelope"; - /** Format: int64 */ - signatureFlow: number; + /** + * Format: int64 + * @enum {integer} + */ + signatureFlow: 0 | 1 | 2; /** Format: int64 */ docmdpLevel: number; /** Format: int64 */ From 7fc9bedf3239fd7b1128d59949cdca580e95e23f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 06/12] fix(validation): accept optional nullable signer email in parser Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/services/validationDocument.ts | 157 +++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/src/services/validationDocument.ts b/src/services/validationDocument.ts index 4cfcb816cb..62a22d30d3 100644 --- a/src/services/validationDocument.ts +++ b/src/services/validationDocument.ts @@ -32,6 +32,8 @@ export type ValidationModificationInfo = { valid?: boolean } +type ValidationSignatureFlow = ValidationFileRecord['signatureFlow'] + type ValidationMetadataDimension = { w: number h: number @@ -64,15 +66,12 @@ function isOptionalField(record: UnknownRecord, key: string, guard: (value: unkn } function toNumber(value: unknown): number | null { - if (typeof value === 'number' && Number.isFinite(value)) { - return value - } - - if (typeof value === 'string' && /^-?\d+$/.test(value)) { - return Number.parseInt(value, 10) - } + return typeof value === 'number' && Number.isFinite(value) ? value : null +} - return null +function toInteger(value: unknown): number | null { + const normalized = toNumber(value) + return normalized !== null && Number.isInteger(normalized) ? normalized : null } function isString(value: unknown): value is string { @@ -84,7 +83,7 @@ function isNullableString(value: unknown): value is string | null { } function isValidationStatus(value: unknown): value is ValidationStatus { - const normalizedValue = toNumber(value) + const normalizedValue = toInteger(value) return normalizedValue === FILE_STATUS.DRAFT || normalizedValue === FILE_STATUS.ABLE_TO_SIGN || normalizedValue === FILE_STATUS.PARTIAL_SIGNED @@ -93,19 +92,22 @@ function isValidationStatus(value: unknown): value is ValidationStatus { } function isSignerStatus(value: unknown): value is SignerDetailRecord['status'] { - const normalizedValue = toNumber(value) + const normalizedValue = toInteger(value) return normalizedValue === SIGN_REQUEST_STATUS.DRAFT || normalizedValue === SIGN_REQUEST_STATUS.ABLE_TO_SIGN || normalizedValue === SIGN_REQUEST_STATUS.SIGNED } -function isValidationSignatureFlow(value: unknown): boolean { - if (value === 'none' || value === 'parallel' || value === 'ordered_numeric') { - return true +function isValidationSignatureFlow(value: unknown): value is ValidationSignatureFlow { + return value === 0 || value === 1 || value === 2 +} + +function normalizeValidationSignatureFlow(value: unknown): ValidationSignatureFlow | null { + if (isValidationSignatureFlow(value)) { + return value } - const normalizedValue = toNumber(value) - return normalizedValue === 0 || normalizedValue === 1 || normalizedValue === 2 + return null } function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo { @@ -113,7 +115,7 @@ function isValidationStatusInfo(value: unknown): value is ValidationStatusInfo { return false } - return isOptionalField(value, 'id', fieldValue => typeof fieldValue === 'number') + return isOptionalField(value, 'id', fieldValue => toInteger(fieldValue) !== null) && isOptionalField(value, 'label', isString) } @@ -153,7 +155,7 @@ function isValidationMetadata(value: unknown): value is NonNullable { + const childId = toInteger(file.id) + const childNodeId = toInteger(file.nodeId) + const childSize = toInteger(file.size) + const childStatus = toInteger(file.status) + const metadataPages = toInteger(file.metadata.p) + + if ( + childId === null + || childNodeId === null + || childSize === null + || childStatus === null + || metadataPages === null + ) { + return null + } + + return { + ...file, + id: childId, + nodeId: childNodeId, + size: childSize, + status: childStatus, + metadata: { + ...file.metadata, + p: metadataPages, + }, + } + }) + + if (files.some(file => file === null)) { + return null + } + const normalizedFiles = files.filter((file): file is ValidatedChildFileRecord => file !== null) + const metadata = isValidationMetadata(data.metadata) ? data.metadata : { ...DEFAULT_VALIDATION_METADATA, - p: data.totalPages, + p: totalPages, } + const metadataPages = toInteger(metadata.p) + if (metadataPages === null) { + return null + } + const settings = isValidationSettings(data.settings) ? data.settings : DEFAULT_VALIDATION_SETTINGS - const signers = Array.isArray(data.signers) ? data.signers : [] + const signers = (Array.isArray(data.signers) ? data.signers : []).map((signer) => { + const signRequestId = toInteger(signer.signRequestId) + const status = toInteger(signer.status) + + if (signRequestId === null || status === null) { + return null + } + + return { + ...signer, + signRequestId, + status, + } + }) + + if (signers.some(signer => signer === null)) { + return null + } + const normalizedSigners = signers.filter((signer): signer is SignerDetailRecord => signer !== null) return { ...data, - metadata, + id, + nodeId, + signatureFlow, + docmdpLevel, + filesCount, + totalPages, + size, + files: normalizedFiles, + metadata: { + ...metadata, + p: metadataPages, + }, settings, - signers, + signers: normalizedSigners, } } From c9406022519369797feb0d69ae77b3a3bd2bda70 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 07/12] test(validation): cover nullable and missing signer email Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/services/validationDocument.spec.ts | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/tests/services/validationDocument.spec.ts b/src/tests/services/validationDocument.spec.ts index 832a01b877..1712b2c5e0 100644 --- a/src/tests/services/validationDocument.spec.ts +++ b/src/tests/services/validationDocument.spec.ts @@ -106,18 +106,33 @@ describe('validationDocument', () => { })) }) - it('accepts validation payload when signatureFlow is enum string and numeric fields are numeric strings', () => { + it('rejects payload with string values in numeric contract fields', () => { const normalized = toValidationDocument(createValidationPayload({ - signatureFlow: 'none', - docmdpLevel: '2', + id: '100', + nodeId: '100', + signatureFlow: 'parallel', + docmdpLevel: '0', filesCount: '1', totalPages: '1', size: '10', - signers: [createSigner({ status: '2' })], + signers: [createSigner({ signRequestId: '1', status: '1' })], + files: [{ + id: '100', + uuid: '550e8400-e29b-41d4-a716-446655440000', + name: 'contract.pdf', + status: '1', + statusText: 'Pending', + nodeId: '100', + totalPages: 1, + size: '10', + pdfVersion: '1.7', + signers: [], + file: '/apps/libresign/p/pdf/550e8400-e29b-41d4-a716-446655440000', + metadata: { extension: 'pdf', p: '1' }, + }], })) - expect(normalized).not.toBeNull() - expect(normalized?.signatureFlow).toBe('none') + expect(normalized).toBeNull() }) it('rejects payload with invalid signer status', () => { @@ -128,6 +143,67 @@ describe('validationDocument', () => { expect(normalized).toBeNull() }) + it('accepts production-shaped payload when signer email is null', () => { + const normalized = toValidationDocument(createValidationPayload({ + uuid: '72a2d63b-772b-4ed7-8b79-5d5b1549fd15', + id: 551, + nodeId: 28111, + signatureFlow: 0, + docmdpLevel: 2, + filesCount: 1, + totalPages: 1, + size: 182873, + status: FILE_STATUS.SIGNED, + statusText: 'Signed', + created_at: '2026-04-23T20:46:58+00:00', + requested_by: { userId: 'owner', displayName: null }, + signers: [createSigner({ + signRequestId: 637, + displayName: 'External signer', + email: null, + status: SIGN_REQUEST_STATUS.SIGNED, + statusText: 'Signed', + request_sign_date: '2026-04-23T20:46:58+00:00', + me: false, + })], + files: [{ + id: 551, + uuid: '72a2d63b-772b-4ed7-8b79-5d5b1549fd15', + name: 'document.pdf', + status: FILE_STATUS.SIGNED, + statusText: 'Signed', + nodeId: 28111, + totalPages: 1, + size: 182873, + pdfVersion: '1.7', + signers: [], + file: '/apps/libresign/p/pdf/72a2d63b-772b-4ed7-8b79-5d5b1549fd15', + metadata: { + extension: 'pdf', + p: 1, + d: [{ w: 595.3, h: 841.9 }], + pdfVersion: '1.7', + status_changed_at: '2026-04-23T20:57:29+00:00', + }, + }], + })) + + expect(normalized).not.toBeNull() + expect(normalized?.signers[0]?.email).toBeNull() + }) + + it('accepts payload when signer email is omitted', () => { + const signerWithoutEmail = createSigner() + delete signerWithoutEmail.email + + const normalized = toValidationDocument(createValidationPayload({ + signers: [signerWithoutEmail], + })) + + expect(normalized).not.toBeNull() + expect(Object.prototype.hasOwnProperty.call(normalized?.signers[0] ?? {}, 'email')).toBe(false) + }) + it.each([ ['status null', { status: null }], ['status outside allowed values', { status: 99 }], From b7e33c1dc592a3d5c16f124e4c50022299b43cab Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 08/12] fix(types): allow nullable signer email in visible elements service Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/services/visibleElementsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/visibleElementsService.ts b/src/services/visibleElementsService.ts index bffd835603..6524c74227 100644 --- a/src/services/visibleElementsService.ts +++ b/src/services/visibleElementsService.ts @@ -13,7 +13,7 @@ import type { export type SignerLike = { signRequestId?: number displayName?: string - email?: string + email?: string | null identifyMethods?: IdentifyMethodRecord[] signed?: string | null | boolean | unknown[] status?: number From 30df597d0c42762f3abfa42a1aeb0909ee4e3842 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:07 -0300 Subject: [PATCH 09/12] fix(validation-ui): support nullable signer email in signer details model Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index 0088c45e03..89e2ef89c2 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -284,7 +284,7 @@ type SignerModifications = { type SignerModel = { displayName?: string - email?: string + email?: string | null name?: string remote_address?: string user_agent?: string From db25852ccbeccf40ce861a9b869c3f08ea08ca29 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Sat, 25 Apr 2026 11:00:08 -0300 Subject: [PATCH 10/12] fix(request-ui): guard avatar user prop for nullable signer email Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/Request/SignDetail/partials/SignerRow.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Request/SignDetail/partials/SignerRow.vue b/src/components/Request/SignDetail/partials/SignerRow.vue index 391cff8478..b3d9bf4aa7 100644 --- a/src/components/Request/SignDetail/partials/SignerRow.vue +++ b/src/components/Request/SignDetail/partials/SignerRow.vue @@ -11,7 +11,7 @@