Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a949d5a
fix(openapi): type signing inputs and honor optional code parameters
vitormattos Nov 14, 2026
c87accf
test(openapi): validate generated schemas and signing SDK types
vitormattos Nov 14, 2026
42b4cd2
fix(openapi): type request policy overrides and active context
vitormattos Nov 14, 2026
72f7067
chore(openapi): regenerate request-policy contracts
vitormattos Nov 14, 2026
0a92c25
fix(openapi): document dynamically configured code identification met…
vitormattos Nov 14, 2026
03dc1c6
chore(openapi): regenerate signing-code identification types
vitormattos Nov 14, 2026
108526f
fix(openapi): preserve reusable policy models and replace geolocation…
vitormattos Nov 14, 2026
e853d8f
chore(openapi): regenerate reusable policy and location contracts
vitormattos Nov 14, 2026
fdadc31
fix(openapi): document id-doc approval as query-only signing context
vitormattos Nov 14, 2026
8256fa3
fix(openapi): expose typed recursive JSON account and policy values
vitormattos Nov 14, 2026
bd32b51
fix(openapi): express nullable JSON string as valid named type
vitormattos Nov 14, 2026
1af90b0
test(openapi): check nullable JSON reference properties
vitormattos Nov 14, 2026
23bcd07
fix(openapi): retain supported approval body input alongside query co…
vitormattos Nov 14, 2026
0373d15
style(signing): retain controller helper indentation
vitormattos Nov 14, 2026
ae3d02c
fix(openapi): avoid unsupported recursive Psalm aliases
vitormattos Nov 14, 2026
9ee0cfc
refactor(openapi): reuse policy value-map schema across compound endp…
vitormattos Nov 14, 2026
6322311
chore(openapi): regenerate shared policy maps and account contract
vitormattos Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions lib/Controller/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* @psalm-import-type LibresignAccountSettingsUpdateResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignActionMessageResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignConfigValueResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignUserConfigValue from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignCreateToSignResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignFile from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
Expand Down Expand Up @@ -409,7 +410,7 @@ public function readPfxData(string $password): DataResponse {
* Set user config value
*
* @param string $key Config key
* @param mixed $value Config value
* @param LibresignUserConfigValue $value Config value as a JSON string, number, boolean, list or nested object. Null is not a storable setting.
* @return DataResponse<Http::STATUS_OK, LibresignConfigValueResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
*
* 200: Config updated
Expand All @@ -419,19 +420,24 @@ public function readPfxData(string $password): DataResponse {
#[NoCSRFRequired]
#[OpenAPI(tags: ['identity'])]
#[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/account/config/{key}', requirements: ['apiVersion' => '(v1)'])]
public function setConfig(string $key): DataResponse {
public function setConfig(string $key, mixed $value): DataResponse {
try {
$user = $this->userSession->getUser();
if (!$user) {
throw new \Exception('User not authenticated');
}
$data = $this->request->getParams();
$value = $data['value'] ?? null;
if ($value === null) {
throw new \InvalidArgumentException('A non-null config value is required.');
}

if (is_bool($value)) {
$value = $value ? '1' : '0';
} elseif (is_array($value)) {
$value = json_encode($value);
$value = json_encode($value, JSON_THROW_ON_ERROR);
} elseif (is_int($value) || is_float($value)) {
$value = (string)$value;
} elseif (!is_string($value)) {
throw new \InvalidArgumentException('Unsupported configuration value.');
}

$this->userConfig->setValueString($user->getUID(), Application::APP_ID, $key, $value);
Expand Down
9 changes: 5 additions & 4 deletions lib/Controller/PolicyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
/**
* @psalm-import-type LibresignErrorResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignEffectivePolicyValue from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignPolicyValueMap from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignEffectivePolicyState from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignEffectivePoliciesResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignEffectiveCompoundPolicyWriteResponse from \OCA\Libresign\ResponseDefinitions
Expand Down Expand Up @@ -346,7 +347,7 @@ public function setGroup(string $groupId, string $policyKey, null|bool|int|float
* them is stored, so the result does not depend on the order of the values.
*
* @param string $parentPolicyKey Policy identifier the other settings are grouped under.
* @param array<string, null|bool|int|float|string|array<string, mixed>> $values Values to persist, keyed by policy identifier, each as an explicit system rule even when it equals the default. Null removes the explicit system rule of that policy: the built-in default applies and lower layers may override it.
* @param LibresignPolicyValueMap $values Values to persist, keyed by policy identifier, each as an explicit system rule even when it equals the default. Null removes the explicit system rule of that policy: the built-in default applies and lower layers may override it.
* @param array<string, bool> $allowChildOverride Whether lower layers may override each saved value, keyed by policy identifier.
* @return DataResponse<Http::STATUS_OK, LibresignEffectiveCompoundPolicyWriteResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
*
Expand Down Expand Up @@ -392,7 +393,7 @@ public function setSystemCompound(string $parentPolicyKey, array $values = [], a
*
* @param string $groupId Group identifier that receives the policy bindings.
* @param string $parentPolicyKey Policy identifier the other settings are grouped under.
* @param array<string, null|bool|int|float|string|array<string, mixed>> $values Values to persist for the group, keyed by policy identifier.
* @param LibresignPolicyValueMap $values Values to persist for the group, keyed by policy identifier.
* @param array<string, bool> $allowChildOverride Whether users and requests below this group may override each saved value, keyed by policy identifier.
* @return DataResponse<Http::STATUS_OK, LibresignGroupCompoundPolicyWriteResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_FORBIDDEN, LibresignErrorResponse, array{}>
*
Expand Down Expand Up @@ -452,7 +453,7 @@ public function setGroupCompound(string $groupId, string $parentPolicyKey, array
* Save several values of the same composite policy as user preferences
*
* @param string $parentPolicyKey Policy identifier the other settings are grouped under.
* @param array<string, null|bool|int|float|string|array<string, mixed>> $values Values to persist as the current user's defaults, keyed by policy identifier.
* @param LibresignPolicyValueMap $values Values to persist as the current user's defaults, keyed by policy identifier.
* @return DataResponse<Http::STATUS_OK, LibresignEffectiveCompoundPolicyWriteResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>
*
* 200: OK
Expand Down Expand Up @@ -497,7 +498,7 @@ public function setUserPreferenceCompound(string $parentPolicyKey, array $values
*
* @param string $userId Target user identifier that receives the policy assignments.
* @param string $parentPolicyKey Policy identifier the other settings are grouped under.
* @param array<string, null|bool|int|float|string|array<string, mixed>> $values Values to persist for the target user, keyed by policy identifier.
* @param LibresignPolicyValueMap $values Values to persist for the target user, keyed by policy identifier.
* @param array<string, bool> $allowChildOverride Whether the target user may still override each assigned value, keyed by policy identifier.
* @return DataResponse<Http::STATUS_OK, LibresignUserCompoundPolicyWriteResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignErrorResponse, array{}>|DataResponse<Http::STATUS_FORBIDDEN, LibresignErrorResponse, array{}>
*
Expand Down
5 changes: 3 additions & 2 deletions lib/Controller/RequestSignatureController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

/**
* @psalm-import-type LibresignNewFile from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignRequestPolicy from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignNewSigner from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignDetailedFile from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignDetailedFileResponse from \OCA\Libresign\ResponseDefinitions
Expand Down Expand Up @@ -66,7 +67,7 @@ public function __construct(
* @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files). Each file supports nodeId, url, base64 or path.
* @param string|null $callback URL that will receive a POST after the document is signed
* @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
* @param array<string, mixed>|null $policy Structured policy payload with request-level overrides and active context.
* @param LibresignRequestPolicy|null $policy Structured policy payload with request-level overrides and active context.
* @return DataResponse<Http::STATUS_OK, LibresignDetailedFileResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse|LibresignActionErrorResponse, array{}>
*
* 200: OK
Expand Down Expand Up @@ -136,7 +137,7 @@ public function requestSignature(
* @param list<LibresignVisibleElement>|null $visibleElements Visible elements on document
* @param LibresignNewFile|null $file File object. Supports nodeId (a non-negative integer or its canonical decimal string), url, base64 or path when creating a new request.
* @param integer|null $status Numeric code of status * 0 - no signers * 1 - signed * 2 - pending
* @param array<string, mixed>|null $policy Structured policy payload with request-level overrides and active context.
* @param LibresignRequestPolicy|null $policy Structured policy payload with request-level overrides and active context.
* @param string|null $name The name of file to sign
* @param LibresignFolderSettings $settings Settings to define how and where the file should be stored
* @param list<LibresignNewFile> $files Multiple files to create an envelope (optional, use either file or files). Each file supports nodeId, url, base64 or path.
Expand Down
46 changes: 28 additions & 18 deletions lib/Controller/SignFileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
* @psalm-import-type LibresignSignActionResponse from ResponseDefinitions
* @psalm-import-type LibresignSignatureRejectionResponse from ResponseDefinitions
* @psalm-import-type LibresignSignerDeviceGeolocation from ResponseDefinitions
* @psalm-import-type LibresignSigningElement from ResponseDefinitions
*/

class SignFileController extends AEnvironmentAwareOCSController implements ISignatureUuid {
Expand Down Expand Up @@ -89,12 +90,11 @@ public function __construct(
*
* @param int $fileId Id of LibreSign file
* @param string $method Signature method
* @param array<string, mixed> $elements List of visible elements
* @param list<LibresignSigningElement> $elements List of visible elements
* @param string $identifyValue Identify value
* @param string $token Token, commonly send by email
* @param bool $async Execute signing asynchronously when possible
* @param LibresignSignerDeviceGeolocation $deviceGeolocation Device-reported geolocation metadata submitted by the signing client
* @psalm-param array<string, mixed> $deviceGeolocation
* @param LibresignSignerDeviceGeolocation|null $deviceGeolocation Optional device geolocation, omitted when absent; legacy [] is accepted
* @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
*
* 200: OK
Expand All @@ -106,22 +106,24 @@ public function __construct(
#[PublicPage]
#[OpenAPI(tags: ['signing'])]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}', requirements: ['apiVersion' => '(v1)'])]
public function signByFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, array $deviceGeolocation = []): DataResponse {
return $this->sign($method, $elements, $identifyValue, $token, $fileId, null, $async, $deviceGeolocation);
public function signByFileId(int $fileId, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, ?array $deviceGeolocation = null): DataResponse {
return $this->sign($method, $elements, $identifyValue, $token, $fileId, null, $async, $deviceGeolocation ?? []);
}

/**
* Sign a file using file UUID
*
* Identification-document approvals use the query string ?idDocApproval=true.
* The signer middleware resolves that query before the controller runs.
*
* @param string $uuid UUID of LibreSign file
* @param string $method Signature method
* @param array<string, mixed> $elements List of visible elements
* @param list<LibresignSigningElement> $elements List of visible elements
* @param string $identifyValue Identify value
* @param string $token Token, commonly send by email
* @param bool $async Execute signing asynchronously when possible
* @param LibresignSignerDeviceGeolocation $deviceGeolocation Device-reported geolocation metadata submitted by the signing client
* @param LibresignSignerDeviceGeolocation|null $deviceGeolocation Optional device geolocation, omitted when absent; legacy [] is accepted
* @param bool $saveSignature Explicitly save the external email signer's visible signature for future documents
* @psalm-param array<string, mixed> $deviceGeolocation
* @return DataResponse<Http::STATUS_OK, LibresignSignActionResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignSignActionErrorResponse, array{}>
*
* 200: OK
Expand All @@ -133,8 +135,8 @@ public function signByFileId(int $fileId, string $method, array $elements = [],
#[PublicPage]
#[OpenAPI(tags: ['signing'])]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}', requirements: ['apiVersion' => '(v1)'])]
public function signBySignerUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, array $deviceGeolocation = [], bool $saveSignature = false): DataResponse {
return $this->sign($method, $elements, $identifyValue, $token, null, $uuid, $async, $deviceGeolocation, $saveSignature);
public function signBySignerUuid(string $uuid, string $method, array $elements = [], string $identifyValue = '', string $token = '', bool $async = false, ?array $deviceGeolocation = null, bool $saveSignature = false): DataResponse {
return $this->sign($method, $elements, $identifyValue, $token, null, $uuid, $async, $deviceGeolocation ?? [], $saveSignature);
}

/**
Expand All @@ -153,7 +155,7 @@ public function sign(
): DataResponse {
try {
$user = $this->userSession->getUser();
$isIdDocApproval = $this->request->getParam('idDocApproval') === 'true';
$isIdDocApproval = $this->isIdentificationDocumentApproval();

if ($isIdDocApproval && $signRequestUuid) {
$libreSignFile = $this->signFileService->getFileByUuid($signRequestUuid);
Expand Down Expand Up @@ -447,10 +449,14 @@ public function signRenew(string $method): DataResponse {
/**
* Get code to sign the document using UUID
*
* For identification-document approvals the frontend sends ?idDocApproval=true
* in the URL query. Nextcloud also accepts the same flag in the JSON body.
* The signer middleware reads the combined request parameters.
*
* @param string $uuid UUID of LibreSign file
* @param 'account'|'email'|null $identifyMethod Identify signer method
* @param string|null $identifyMethod Configured signer identification method (for example account, email or sms); omitted methods remain subject to service validation
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
* @param 'true'|null $idDocApproval Literal string "true" when the UUID is an identification-document file UUID (matches middleware RequireSigner)
* @param string|null $idDocApproval Only the literal "true" selects the identification-document approval context. Query or JSON body is accepted.
* @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
*
* 200: OK
Expand All @@ -464,12 +470,12 @@ public function signRenew(string $method): DataResponse {
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/uuid/{uuid}/code', requirements: ['apiVersion' => '(v1)'])]
public function requestCodeBySignerUuid(
string $uuid,
?string $identifyMethod,
?string $signMethod,
?string $identifyMethod = null,
?string $signMethod = null,
?string $idDocApproval = null,
): DataResponse {
try {
if ($idDocApproval === 'true') {
if ($this->isIdentificationDocumentApproval($idDocApproval)) {
// In this context the uuid is the one of the file, not of a sign request.
$libreSignFile = $this->signFileService->getFileByUuid($uuid);
$signRequest = $this->signFileService->getSignRequestToSign($libreSignFile, null, $this->userSession->getUser());
Expand All @@ -491,7 +497,7 @@ public function requestCodeBySignerUuid(
* Get code to sign the document using FileID
*
* @param int $fileId Id of LibreSign file
* @param 'account'|'email'|null $identifyMethod Identify signer method
* @param string|null $identifyMethod Configured signer identification method (for example account, email or sms); omitted methods remain subject to service validation
* @param string|null $signMethod Method used to sign the document, i.e. emailToken, account, clickToSign, smsToken, signalToken, telegramToken, whatsappToken, xmppToken
* @return DataResponse<Http::STATUS_OK, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
*
Expand All @@ -505,7 +511,7 @@ public function requestCodeBySignerUuid(
#[PublicPage]
#[OpenAPI(tags: ['signing'])]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/sign/file_id/{fileId}/code', requirements: ['apiVersion' => '(v1)'])]
public function requestCodeByFileId(int $fileId, ?string $identifyMethod, ?string $signMethod): DataResponse {
public function requestCodeByFileId(int $fileId, ?string $identifyMethod = null, ?string $signMethod = null): DataResponse {
try {
$signRequest = $this->signRequestMapper->getByFileIdAndUserId($fileId);
} catch (\Throwable) {
Expand All @@ -519,6 +525,10 @@ public function requestCodeByFileId(int $fileId, ?string $identifyMethod, ?strin
);
}

private function isIdentificationDocumentApproval(?string $idDocApproval = null): bool {
return $idDocApproval === 'true' || $this->request->getParam('idDocApproval') === 'true';
}

/**
* @todo validate if can request code
* @return DataResponse<Http::STATUS_OK|Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
Expand Down
6 changes: 4 additions & 2 deletions lib/Controller/SignatureElementsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

/**
* @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignSavedSignatureElement from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignSavedSignatureFile from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignUserElement from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignUserElementsMessageResponse from \OCA\Libresign\ResponseDefinitions
* @psalm-import-type LibresignUserElementsResponse from \OCA\Libresign\ResponseDefinitions
Expand Down Expand Up @@ -73,7 +75,7 @@ public function __construct(
/**
* Create signature element
*
* @param array<string, mixed> $elements Element object
* @param list<LibresignSavedSignatureElement> $elements Saved signature elements
* @return DataResponse<Http::STATUS_OK, LibresignUserElementsMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
*
* 200: OK
Expand Down Expand Up @@ -314,7 +316,7 @@ public function getSignatureElement(int $nodeId): DataResponse {
*
* @param int $nodeId Node id of a Nextcloud file
* @param string $type The type of signature element
* @param array<string, mixed> $file Element object
* @param LibresignSavedSignatureFile $file Signature image supplied as base64 or URL
* @return DataResponse<Http::STATUS_OK, LibresignUserElementsMessageResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignMessageResponse, array{}>
*
* 200: OK
Expand Down
Loading
Loading