Electronic Signature contracts
Look up the App contribution, document, participant, request, result, and error contracts for NEXIA Electronic Signature.
Electronic Signature Contracts
Choose the flow before constructing a request: publish a template binding and data source for an App-owned document, or supply an uploaded PDF through the request contract. Resolve SignatureDocumentHost or SignatureHost from the container. Treat returned status and authorized links as the result; creating a document does not mean signatures are complete.
Read an existing request
Use the restored actor and Legal Entity plus your App-owned subject reference.
$requestPublicId identifies the request; it does not grant access.
use Nexia\Signature\Contracts\SignatureHost;
use Nexia\Signature\SignatureRequestQueryInput;
$query = new SignatureRequestQueryInput(
legalEntity: $legalEntity,
actor: $actor,
subject: $subject,
requestPublicId: $requestPublicId,
);
$summary = app(SignatureHost::class)->summary($query);The host returns a SignatureRequestSummary or null. Handle absence without
exposing another subject's request. To create a document or submit a request, use
the contribution and submission contracts below.
Signature
Apps import only these public SDK interfaces; Core binds their host implementations.
interface SignatureDocumentHost
{
public function createCurrentBound(CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
public function replace(SignableDocumentReference $predecessor, CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
public function rebuildPreReady(PreReadySignableDocumentReference $predecessor, CurrentBoundSignableDocumentSubmission $submission): CurrentBoundSignableDocumentResult;
public function retryPreReady(PreReadySignableDocumentReference $document, Actor $actor, string $reasonCode): CurrentBoundSignableDocumentResult;
public function summary(string $publicId, Actor $actor): ?SignableDocumentSummary;
public function previewHrefIfAuthorized(string $publicId, Actor $actor): ?string;
public function sourceDownloadHrefIfAuthorized(string $publicId, Actor $actor): ?string;
public function cancel(string $publicId, Actor $actor): SignableDocumentSummary;
}
interface SignatureHost
{
public function submit(SignatureRequestSubmission $submission): SignatureRequestResult;
public function summary(SignatureRequestQueryInput $query): ?SignatureRequestSummary;
public function requestDetailHrefIfAuthorized(SignatureRequestQueryInput $query): ?string;
public function completedDocumentDetailHrefIfAuthorized(SignatureRequestQueryInput $query): ?string;
public function cancel(SignatureRequestCancelInput $input): SignatureRequestActionResult;
public function resend(SignatureRequestResendInput $input): SignatureRequestActionResult;
public function reissue(SignatureRequestReissueInput $input): SignatureRequestResult;
}Source paths:
packages/app-sdk/packages/laravel/src/Signature/Contracts/SignatureDocumentHost.php and
packages/app-sdk/packages/laravel/src/Signature/Contracts/SignatureHost.php.
rebuildPreReady() cancels a queued/rendering revision and creates one successor
from the current template winner. retryPreReady() instead requeues that exact
retryable or attempts-exhausted revision after the owning workflow reauthorizes
it. completedDocumentDetailHrefIfAuthorized() returns a protected UI route
only for a complete request whose finalized artifact the current actor may read.
Additional source-neutral interfaces cover source plans, credentials, template selection, participant policy, and request-local preparation:
interface SignatureDocumentPlanHost
{
public function submit(SignatureDocumentPlanSubmission $submission): CurrentBoundSignableDocumentResult|PreparedSignableDocumentResult;
}
interface SignatureRequestCredentialHost
{
public function deriveRequestPasswordVerifier(string $password): SignatureRequestPasswordVerifier;
}
interface SignatureTemplateCatalogHost
{
public function eligible(SignatureTemplateCatalogQuery $query): SignatureTemplateCatalogResult;
}
interface SignatureTemplateParticipantAssignmentHost
{
public function resolve(SignatureTemplateParticipantAssignmentQuery $query): SignatureTemplateParticipantAssignmentResult;
}
interface SignatureRequestPreparationHost
{
public function begin(SignatureRequestPreparationSubmission $submission): SignatureRequestPreparationReference;
public function submit(SignatureRequestPreparationReference $preparation, SignatureRequestSubmission $submission): SignatureRequestResult;
}PreparedSignableDocumentHost is the lower-level uploaded-PDF bridge used by
the plan host. Prefer SignatureDocumentPlanHost when App code intentionally
supports both published-template and uploaded-PDF sources.
SignatureRequestPreparationHost starts a request-local editable draft from an
App-authorized canonical snapshot, then accepts the exact ready document with
the ordinary request idempotency contract. Business values remain App-owned;
accept an edited business value through the owning App command and begin a new
preparation. Catalog and participant-assignment queries always carry the actor,
Legal Entity, exact subject, binding version, locale, and effective date.
Template-binding publication
An App publishes each reusable Signature template family as a
SignatureTemplateBindingDescriptor in the immutable set returned by
AppDescriptorContribution::appDescriptors(). The contributor class must live
under a namespace and directory declared by the App Manifest's
contributionLocations(). Core discovers the interface from that location and
filters the set by descriptor class; there is no
SignatureTemplateBindingContribution interface and no manual container or
service-provider registration.
The descriptor's resourceKey links the family to an existing canonical
Resource. It does not publish that Resource. An App-owned subject also needs its
normal Resource contribution, create permission, and
ResourceReferenceResolutionContribution, because Core rechecks both the
permission and exact ResourceRef when it creates or reads a document.
Source paths:
packages/app-sdk/packages/laravel/src/AppDescriptors/AppDescriptorContribution.php,
packages/app-sdk/packages/laravel/src/AppDescriptors/AppDescriptorSet.php, and
packages/app-sdk/packages/laravel/src/AppDescriptors/SignatureTemplateBindingDescriptor.php.
Data-source publication
An App that permits template authors to pull protected values publishes one paired contribution through the same App discovery location:
interface SignatureDocumentDataSourceContribution extends AppDescriptorContribution
{
public function signatureDocumentDataSourceProviders(): array;
}
interface SignatureDocumentDataSourceProvider
{
public function appKey(): string;
public function sourceKey(): string;
public function sourceVersion(): int;
public function resolve(SignatureDocumentDataQuery $query): SignatureDocumentDataResult;
}appDescriptors() returns the static SignatureDocumentDataSourceDescriptor;
signatureDocumentDataSourceProviders() returns its runtime provider. Core
joins the pair only when App, source key, version, contribution class, and App
owner all match.
For a newly authored model-backed source, set the optional descriptor
resourceKey. The registry then requires a labeled ResourceDescriptor with
that exact key and the same discovered App owner, and the catalog serializes the
Resource label for the authoring UI. This is separate from
supportedSubjectResourceKeys, which limits the document subjects compatible
with the source. The descriptor and provider do not make a missing Resource or
subject Resource exist.
packages/people/src/Descriptors/PeopleSignatureDocumentDataSources.php is the
complete in-repository example. PeopleCoreAppManifest::contributionLocations()
covers its Descriptors directory; the class returns four data-source
descriptors and four matching providers for employment-contract, worker,
employment, and worker-assignment Resources.
Bulk-binding publication
An App that supports one exact Group Bulk binding implements
SignatureBulkBindingContribution. Its appDescriptors() publishes
SignatureBulkBindingDescriptor values and
signatureBulkBindingProviders() publishes the matching runtime providers.
Core joins App key, binding key, binding version, and capability contract
version exactly; a descriptor without its provider is unavailable. The provider
must authorize its bounded preview and reauthorize() the final execution
context rather than trusting preview output.
SignatureBulkBindingDescriptor field | Contract |
|---|---|
appKey, bindingKey, bindingVersion, capabilityContractVersion | Exact provider identity; the binding key is owned by the App |
supportedSubjectResourceKeys | One to twenty unique canonical Resource keys |
roleAssignmentPolicies | One to eight uniquely keyed role policies and their allowed assignment sources |
supportedInvitationChannels, supportedAuthenticationMethods | Non-empty capability ceilings enforced by Core |
groupSelectorResourceKey, groupSelectorLabelKey, groupSelectorRequired | Optional paired selector metadata; a required selector must name one supported subject Resource |
status | Descriptor lifecycle; inactive contributions are not available |
Preview receives Core-restored tenant, Legal Entity, actor, template, group
selection, and exact participant slots. The returned execution plan keeps its
document variables, classifications, and signatory-role values out of ordinary
serialization; only protectedPayload() may hand them to encrypted Core
persistence. Final execution uses the provider's fresh reauthorization result,
not the earlier preview snapshot.
Minimal example
The People App declares a discoverable template family in
packages/people/src/Contribution/PeopleSignatureDescriptors.php:
namespace Amuzcorp\Nexia\PeopleCore\Contribution;
use Nexia\AppDescriptors\Contracts\AppDescriptorContribution;
use Nexia\AppDescriptors\AppDescriptorSet;
use Nexia\AppDescriptors\DescriptorStatus;
use Nexia\AppDescriptors\SignatureTemplateBindingDescriptor;
use Nexia\AppDescriptors\SignatureTemplateVariableDescriptor;
use Nexia\AppDescriptors\SignatureSignatoryRoleDescriptor;
use Nexia\Signature\SignatureVariableType;
use Nexia\Signature\SignatureDataClassification;
final class PeopleSignatureDescriptors implements AppDescriptorContribution
{
public static function appDescriptors(): AppDescriptorSet
{
return AppDescriptorSet::of(new SignatureTemplateBindingDescriptor(
appKey: 'people',
bindingKey: 'people.employment_contract',
resourceKey: 'people.employment_contract',
labelKey: 'people.signature_bindings.employment_contract.label',
descriptionKey: 'people.signature_bindings.employment_contract.description',
createPermissionKey: 'people.employment_contract.create_document',
variables: [
new SignatureTemplateVariableDescriptor('worker.full_name', 'people.signature_bindings.employment_contract.variables.worker_full_name', SignatureVariableType::String, true, SignatureDataClassification::Restricted),
new SignatureTemplateVariableDescriptor('contract.starts_on', 'people.signature_bindings.employment_contract.variables.contract_starts_on', SignatureVariableType::Date, true, SignatureDataClassification::Internal),
],
signatoryRoles: [
new SignatureSignatoryRoleDescriptor('worker', 'people.signature_bindings.employment_contract.roles.worker', 1, 1),
],
syntheticSample: [
'worker.full_name' => 'Alex Kim',
'contract.starts_on' => '2026-09-01',
],
templateKeys: ['people.employment_contract.new_hire'],
version: '1.0',
status: DescriptorStatus::Active,
));
}
}This focused form is constructor-valid and shows the minimum relationship. The
real People binding adds the optional employer representative and the remaining
employment-contract variables. The class is discovered only because
PeopleCoreAppManifest::contributionLocations() covers its namespace and
directory. No service provider registers it.
This descriptor registers a Signature template family, not the business
Resource itself. resourceKey must name an existing canonical Resource, and
Core must be able to resolve that exact subject through
ResourceReferenceResolutionContribution before document creation is
authorized. For People, those separate contributions are
Contribution/Resources/EmploymentContractModule.php and
Contribution/ReferenceResolution/PeopleEmploymentContractReferenceResolver.php.
After contribution discovery and tenant template publication, create a document from current App facts:
Handler excerpt: $legalEntity, $actor, and $subject are restored/authorized context; $workerPartyPublicId, $contractPublicId, and $correlationId come from the App record and operation. $workerPartyPublicId is the worker’s person Party public ID, not the Worker Resource ID.
use Nexia\Signature\Contracts\SignatureDocumentHost;
use Nexia\Signature\CurrentBoundSignableDocumentSubmission;
$documents = app(SignatureDocumentHost::class);
$result = $documents->createCurrentBound(new CurrentBoundSignableDocumentSubmission(
legalEntity: $legalEntity,
actor: $actor,
subject: $subject,
bindingKey: 'people.employment_contract',
bindingVersion: '1.0',
templateKey: 'people.employment_contract.new_hire',
locale: 'ko',
effectiveOn: '2026-09-01',
variables: [
'worker.full_name' => 'Alex Kim',
'contract.starts_on' => '2026-09-01',
],
signatoryRoles: [
'worker' => [['party_public_id' => $workerPartyPublicId]],
],
idempotencyKey: 'people.employment-contract.document:'.$contractPublicId,
correlationId: $correlationId,
));CurrentBoundSignableDocumentResult is an acceptance result, not a ready
reference. Consume signature.document.outcome.v1, call summary(), and build
SignableDocumentReference only when status is ready and the stored subject,
revision, and checksum agree.
The published template also freezes its parallel or sequential routing mode
and signer order into that ready document. The App does not add those fields to
SignatureRequestSubmission; Core derives them from expectedDocument and
rejects request-level overrides.
Generate a fail-closed starting pair with a dry run first:
task artisan -- nexia-apps:make-package-signature-data-source assets AssignedAssetLabelsV2 \
--subject-resource-key=people.employment_contract \
--source-resource-key=assets.asset_assignment --cardinality=many \
--min-items=0 --max-items=50 --field-key=asset_display_name \
--field-type=string --field-classification=internal --field-formatter=plainAdd --write only after reviewing the two target paths. The command creates a
descriptor/contribution and a provider that returns unavailable until every
authorization and provenance TODO is implemented; it never reflects a model,
fillable list, cast, column, or relation. Existing targets are refused unless
the explicit --write --force combination is used.
Parameters
SignatureTemplateBindingDescriptor
| Parameter | Type | Required | Default | Behavior |
|---|---|---|---|---|
appKey | string | yes | — | Owning App key |
bindingKey | string | yes | — | App-owned family key; also exposed as key |
resourceKey | string | yes | — | Canonical Resource kind the family binds |
labelKey, descriptionKey | string | yes | — | Localized catalog text keys |
createPermissionKey | string | yes | — | App-owned authority Core rechecks for document creation |
variables | list of SignatureTemplateVariableDescriptor | yes | — | Unique typed variables and their data classification |
signatoryRoles | non-empty list of SignatureSignatoryRoleDescriptor | yes | — | Unique lower-snake-case roles, min/max cardinality, and optional assignment policy |
syntheticSample | keyed array | yes | — | Non-production preview values; every required variable must appear |
templateKeys | non-empty list of string | yes | — | Unique App-owned template families allowed by the binding |
version | string | no | 1.0 | Exact contribution version frozen into documents |
status | DescriptorStatus | no | Active | Contribution lifecycle |
requestNavigationId | string or null | no | null | Optional Shell navigation identity for the App-owned request entry point |
requestNavigationRoute | local absolute route or null | no | null | Optional local route; requires requestNavigationId and rejects //, whitespace, and control characters |
Variable types are string, integer, decimal, boolean, date,
datetime, and money. Classification is public, internal,
confidential, or restricted; only public may appear in logs. Money values
have numeric amount and a three-letter uppercase currency.
Each role's optional SignatureParticipantAssignmentPolicy declares one or
more permitted authorities: binding_resolved, request_supplied, or
template_fixed. It limits how a participant may be supplied; it does not
itself resolve or authorize that participant.
SignatureDocumentDataSourceDescriptor
| Parameter | Required behavior |
|---|---|
appKey, sourceKey, sourceVersion | Exact App-owned keys and positive integer source version; the provider must return the same tuple |
resourceKey | Optional canonical Resource key for the same App. When supplied, it identifies the labeled ResourceDescriptor used in authoring. |
supportedSubjectResourceKeys, lookupMode | Explicit subjects and one of direct_subject, derived_ref, explicit_source_ref, owner_scoped_query |
defaultSourceRefAnchor | Required canonical owner-projected anchor for derived_ref and explicit_source_ref; forbidden for the other modes. Core uses it only to initialize authoring and never guesses a record |
cardinality, stableSortKey | one or bounded many; many requires a declared required deterministic scalar sort field |
providedDataContracts | Optional versioned semantic contracts whose declared field schemas must agree across providers |
fields | Explicit typed, required/optional, classified fields and compatible formatter allowlists |
syntheticSample, label keys | Complete authoring-only sample and App-local locale keys; never live values |
SignatureDocumentDataQuery
Core restores tenant, Legal Entity, actor, purpose, exact subject
ResourceRef, derived anchors, optional explicit source ref, asOf, requested
fields, and selected refs. The provider must independently reauthorize every
protected field and record, prove the subject relationship, apply the App-owned
as-of/state rule, bound the query, and re-resolve selected refs exactly. A
ResourceRef or party_id identifies a candidate; neither grants access.
CurrentBoundSignableDocumentSubmission
| Parameter | Type | Required | Default | Behavior |
|---|---|---|---|---|
legalEntity, actor | SDK contracts | yes | — | Restored current execution context |
subject | ResourceRef | yes | — | App-owned business record Core authorizes |
bindingKey, bindingVersion | string | yes | — | Exact installed descriptor identity |
templateKey | string | yes | — | Current published template family to resolve |
locale | string | yes | — | BCP-47-like locale such as ko or en-US |
effectiveOn | Y-m-d string | yes | — | Selects the effective published template revision |
variables | keyed array | yes | — | Current App values checked against the descriptor |
signatoryRoles | keyed lists | yes | — | Scalar-only business identity snapshots by role |
idempotencyKey | string | yes | — | Same key and payload return the existing command result |
correlationId | UUID string | yes | — | Trace identity |
causationId | UUID or null | no | null | Triggering event identity |
trustedAssets | list | no | [] | Explicit host-governed assets such as an official seal placement |
SignatureRequestSubmission
| Parameter | Type | Required | Default | Behavior |
|---|---|---|---|---|
legalEntity, actor, subject | SDK contracts | yes | — | Must match current tenant, scope, and App resource authority |
expectedDocument | SignableDocumentReference | yes | — | Frozen public id, positive revision, and lowercase 64-character checksum |
participants | non-empty list | yes | — | Immutable SignatureParticipantSnapshot entries whose unique positive sequences match the ready document's frozen signer order |
consentPolicy | SignatureConsentPolicyReference | yes | — | Exact host policy key/version; baseline is standard-consent@1.0 |
expiryPolicy | SignatureExpiryPolicy | yes | — | Explicit-zone ISO-8601 deadline; current action is expire |
idempotencyKey | string | yes | — | Maximum 191 characters |
correlationId | UUID string | yes | — | Trace identity |
causationId | UUID or null | no | null | Triggering event identity |
A participant declares roleKey, positive sequence, unique
assignedFieldKeys, displayName, locale, invitation channel and address,
authentication profile, required methods, and optional partyPublicId for an
internal person Party. That value is an identity assertion, not authority:
Core re-resolves an active person Party in the current tenant and requires its
current contact email to match the invitation address before it creates a
personal-inbox binding. Leave it absent for external recipients; Core never
infers a Party from an email address. Supply the email OTP address when
email_otp is selected. Use SignatureRequestCredentialHost to derive the
opaque requestPasswordVerifier; never hash a request password in the App.
There is deliberately no request routingMode or signingOrder input.
partyPublicId is included in the transport snapshot but excluded from
toLogSafeArray(). Entering through an authenticated Party binding does not
satisfy or remove any frozen email_otp or request_password method.
Options
Document source
| Source | Contract | Use when |
|---|---|---|
| Published template | SignatureDocumentHost::createCurrentBound() | The App contributes a stable template family; preferred path |
| Composable plan | SignatureDocumentPlanHost::submit() with PublishedTemplateSignatureDocumentSource | One action deliberately selects among source kinds |
| Uploaded PDF | Plan host with UploadedPdfSignatureDocumentSource | A one-off PDF and explicit normalized field rectangles are required |
An uploaded PDF uses a private uploadIntentId, participant snapshots, and
SignatureFieldDefinition entries. Rectangles use a one-based page and
normalized x, y, width, and height within 0..1. Available field kinds
are text, checkbox, date, select, signer_name, signature, and
signed_at. Optional datePart is presentation-only: date accepts year,
month, or day, while signed_at also accepts date and time; other
field kinds reject it.
Authentication and delivery
| Profile | Required methods | Current availability |
|---|---|---|
standard_password@1.0 | request_password | Operational when local password derivation is ready |
standard_email_otp@1.0 | email_otp | Operational when mail and queues are ready |
standard_password_email_otp@1.0 | request_password and email_otp | Operational when both dependencies are ready |
standard_sms_otp@1.0 | sms_otp | Unavailable |
Email is the supported invitation channel. Do not accept sms or sms_otp
merely because they exist in the enums; Core rejects unavailable capabilities.
Data-source versioning
A breaking field, meaning, cardinality, lookup, or provenance change gets a new
positive source version. Keep the exact old descriptor/provider operational
while a published template revision still names it, or publish a successor
template and migrate new preparations explicitly. Validate the assembled
contract and all App-local locale keys with
php artisan nexia-apps:validate-app-descriptors <app-key>.
Routing and progress
| Contract | Values or fields | Meaning |
|---|---|---|
SignatureRoutingMode | parallel, sequential | Immutable policy copied from the published template through the document into the request |
SignatureRequestResult::routingMode | SignatureRoutingMode | Accepted request's frozen mode; legacy serialized results default to parallel |
SignatureRequestSummary::routingMode | SignatureRoutingMode | Authorized, log-safe request mode |
SignatureParticipantProgress | roleKey, roleSlot, sequence, status, routingState, presentedRevision, completedAt | Log-safe participant identity, execution order, routing eligibility, and request-local PDF revision ordinal |
SignatureParticipantRoutingState | waiting, revision_pending, active, completed, closed, failed | Whether a participant is waiting for a predecessor, waiting for a verified cumulative PDF, eligible now, or terminal |
In parallel, every participant is active against request-local revision 0.
In sequential, participant N becomes active only after verified revision
N - 1 exists. presentedRevision reports the exact ordinal that participant
saw; it does not grant artifact access or expose PDF content or a checksum.
Output or return
| Call | Return | Meaning |
|---|---|---|
createCurrentBound, replace, rebuildPreReady | CurrentBoundSignableDocumentResult | Public id, accepted|existing, document status, frozen binding/template versions, revision |
SignatureDocumentHost::summary | ?SignableDocumentSummary | Authorized non-content state, resolved fields, page count, checksum/failure, attempts, trusted assets |
SignatureHost::submit, reissue | SignatureRequestResult | Public id, accepted|existing, lifecycle status, subject, exact document, optional predecessor, frozen routing mode |
SignatureHost::summary | ?SignatureRequestSummary | Frozen routing mode, log-safe participant routing/progress and presented revision, expiry, failure code, and completed artifact reference |
cancel, resend | SignatureRequestActionResult | Action, accepted|existing, and resulting request status |
| authorized href methods | ?string | Protected Core route, or null when current authority does not hold |
Document statuses are queued, rendering, ready, retryable_failed,
terminal_failed, cancelled, and superseded. Request terminal statuses are
completed, declined, expired, cancelled, and failed; preceding states
are draft, preparing, ready, dispatching, in_progress, and
finalizing.
SignatureRequestSummary::toLogSafeArray() excludes participant names,
invitation and OTP addresses, credential material, and document content. Do not
replace it with SignatureParticipantSnapshot::toArray() in diagnostics.
Errors
Host failures use SignatureException; branch on errorCode, not message text.
SignatureErrorCode | Cause | Resolution |
|---|---|---|
unauthorized, request_scope_mismatch | Actor, Legal Entity, subject, or App authority is stale | Restore current context and reauthorize the business record |
binding_unavailable, template_unavailable | Descriptor/version is absent or no eligible published template exists | Verify contribution discovery, exact version, locale, and effective date |
document_not_ready, document_reference_mismatch | Request used an unfinished or different revision/checksum | Consume the document outcome and rebuild the reference from an authorized summary |
payload_drift, request_conflict | An idempotency identity was reused with different frozen input | Keep one canonical payload per key; create an explicit successor for change |
participant_invalid, participant_assignment_invalid | Role cardinality, sequence, or field ownership does not match | Rebuild participants from the binding and ready document assignments |
authentication_profile_unavailable, authentication_methods_invalid | Profile version and required methods differ | Submit the exact profile method set |
unsupported_capability | SMS or a runtime dependency is unavailable | Select an operational capability; do not bypass the host gate |
consent_policy_unavailable, expiry_policy_invalid | Policy version is missing or deadline/action is invalid | Use a published policy and explicit-zone future timestamp |
request_terminal | Mutation targeted a terminal request | Start reissue() when a valid successor is needed |
execution_unavailable, rate_limited | Bounded runtime or delivery failure | Preserve the handoff and retry through an authorized idempotent path |
artifact_verification_failed | Completed PDF or manifest identity failed verification | Stop business application and escalate as an integrity incident |
DTO constructors throw InvalidArgumentException before a host call for shape
errors such as non-UUID correlation ids, list/keyed-array confusion, duplicate
sequences, invalid locale/date/timestamp formats, or out-of-page rectangles.
Real usage
packages/people/src/Actions/SubmitEmploymentContractDocumentHandoff.php
restores a delegated actor, rechecks
people.employment_contract.create_document, and calls
createCurrentBound() only after its Inbox reservation commits. It compares
the returned binding, version, and template before accepting the handoff.
packages/people/src/Actions/SubmitEmploymentContractSignatureHandoff.php
then rebuilds SignatureRequestSubmission from the durable App handoff. It
checks the current SignableDocumentReference, calls submit() or reissue(),
and rejects a result whose subject, document, checksum, or predecessor differs.
It submits the worker as sequence 1 and employer representative as sequence 2;
it does not choose a routing mode. The published template remains the routing
authority.
Finally,
packages/people/src/Actions/ApplyEmploymentContractSignatureOutcome.php
consumes signature.request.accepted.v1 and
signature.request.outcome.v1, calls both authorized summaries before the App
Inbox transaction, and applies one projection only after current actor,
permission, subject, expiry, document, and artifact facts agree.
For a data-source implementation, compare
packages/people/src/Descriptors/PeopleSignatureDocumentDataSources.php with
packages/people/src/Signature/WorkerAssignmentsSignatureDocumentDataSourceProvider.php.
The descriptor declares the exact employment-contract subject, derived Worker
anchor, bounded many-cardinality fields, and stable sort key. The provider then
rechecks the tenant, Legal Entity, actor, subject and selected references,
applies the as-of window, bounds at 50, and sorts by effective date plus public
ID.
Verify this integration with:
php artisan test packages/people/tests/Feature/EmploymentContractSignatureBridgeTest.phpRelated
- Electronic Signature — trace the end-to-end Core, SDK, and App ownership model
- Contribution contracts — discovery locations and the
AppDescriptorContributionpublication boundary - Handle events and jobs — persist calls and outcomes across transaction boundaries
- Resource References — construct the canonical subject carried by every document and request