#Configuration
After publishing, the config file lives at config/signature.php.
Your copy is merged with the package defaults one level deep. A top-level block
you delete falls back to the default. A block you keep replaces the default
whole, along with the env variables inside it. Keep blocks complete, or
remove them and use .env. See
Keep your published config current.
#Certificate driver
'cert_driver' => env('SIGNATURE_CERT_DRIVER', 'openssl'),| Value | Description |
|---|---|
openssl | Self-signed via PHP's openssl_* extension (default) |
cfssl | Issues certificates from a running CFSSL server |
#OpenSSL options
'openssl' => [
'digest_alg' => 'sha256',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'cert_lifetime' => 3650, // days before expiry
'ca_cert_path' => storage_path('app/certs/ca.crt'), // optional
'ca_key_path' => storage_path('app/certs/ca.key'), // optional
],When ca_cert_path and ca_key_path point to valid files, certificates are CA-signed. Leave absent for development (self-signed).
#CFSSL options
'cfssl' => [
'host' => env('CFSSL_HOST', 'http://localhost:8888'),
'profile' => env('CFSSL_PROFILE', 'client'),
],#PDF driver
'pdf_driver' => env('SIGNATURE_PDF_DRIVER', 'fpdi'),| Value | Description |
|---|---|
fpdi | FPDI + TCPDF — imports existing PDF pages, embeds PKCS#7 signature (default) |
tcpdf | Pure TCPDF |
#Storage
'storage_disk' => env('SIGNATURE_DISK', 'local'),
'certs_path' => 'certs', // PFX certificate files
'signatures_path' => 'signatures', // raw signature images
'signed_docs_path' => 'signed-docs', // completed signed PDFs
'preview_url_ttl' => 5, // minutes a signature preview link stays validAll paths are relative to the disk root. Any Laravel disk driver works (local, s3, etc.).
#Image constraints
Applied both client-side (JS) and server-side (PHP) during upload validation.
'image' => [
'max_kb' => 512,
'allowed_mimes' => ['image/png', 'image/jpeg'],
'canvas_width' => 600,
'canvas_height' => 200,
],#Admin Resource
Controls whether and how the built-in Signatures resource appears in the panel.
'resource' => [
'enabled' => env('SIGNATURE_RESOURCE_ENABLED', true),
'navigation_icon' => env('SIGNATURE_RESOURCE_ICON', 'heroicon-o-pencil-square'),
'navigation_group' => env('SIGNATURE_RESOURCE_GROUP', null),
'navigation_sort' => env('SIGNATURE_RESOURCE_SORT', null),
'navigation_label' => env('SIGNATURE_RESOURCE_LABEL', 'Signatures'),
],These are the defaults. Values set on SignaturePlugin::make() take precedence:
SignaturePlugin::make()
->navigationGroup('Documents')
->navigationIcon('heroicon-o-pencil-square')
->navigationSort(10)
->navigationLabel('Document Signatures')If you manually register or discover SignatureResource without SignaturePlugin::make(), the resource falls back to these config values. The recommended setup is still to register the plugin on the panel.
#Metadata & machine binding
Every stored signature PNG receives HMAC-signed tEXt chunks and XMP metadata. Machine lock requires the same browser/device on re-upload.
'metadata' => [
'enforce_machine_lock' => env('SIGNATURE_MACHINE_LOCK', true),
],| Setting | Effect |
|---|---|
false | Only verifies HMAC + user ID on re-upload |
true (default) | Also verifies the device fingerprint and DB machine_fingerprint |
#Registered signing devices
Each browser holds a non-extractable P-256 key in IndexedDB and proves to the server that it holds the key. Every signature then records the device it was created on or used on. See security.md §10 and Device registration.
'devices' => [
'enabled' => env('SIGNATURE_DEVICES_ENABLED', true),
'require' => env('SIGNATURE_DEVICES_REQUIRE', 'off'), // off | warn | enforce
'usage_policy' => env('SIGNATURE_DEVICES_USAGE_POLICY', 'any_registered'), // or creation_device_only
'notify_on_new_device' => env('SIGNATURE_DEVICES_NOTIFY', true),
'notification_channels' => ['mail'],
'max_per_user' => 10,
'challenge_ttl' => 120, // seconds a challenge can be answered
'attestation_ttl' => 900, // seconds a browser's proof stays valid
],require | Effect |
|---|---|
off (default) | Record the device when there is one, never block |
warn | The same, and log each signature act that had no verified device |
enforce | Creating or using a signature fails with UnregisteredDeviceException without a verified device |
A revoked device is refused in every mode.
#Desktop agent (Kukux Sign Agent)
Pairs a computer whose signing key lives in its Secure Enclave or TPM. See Desktop agent.
'devices' => [
// …
'agent' => [
'enabled' => env('SIGNATURE_AGENT_ENABLED', false),
'approval' => env('SIGNATURE_AGENT_APPROVAL', 'prefer'), // off | prefer | enforce
'scheme' => 'kukuxsign', // the custom URL scheme the agent registers for pairing links
'download_url' => env('SIGNATURE_AGENT_DOWNLOAD_URL', 'https://github.com/cortejojicoy/digital-signature-agent/releases/latest'),
'min_version' => env('SIGNATURE_AGENT_MIN_VERSION', '0.1.0'),
'require_presence' => env('SIGNATURE_AGENT_REQUIRE_PRESENCE', true),
'server_id' => env('SIGNATURE_AGENT_SERVER_ID'), // null = derived from APP_KEY
'salt' => env('SIGNATURE_AGENT_SALT'), // null = derived from APP_KEY
'pairing_ttl' => 600,
'job_ttl' => 300,
'skip_ttl' => 120,
// Detected device types refused at pairing. Empty = allow VMs.
'blocked_device_types' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('SIGNATURE_AGENT_BLOCKED_DEVICE_TYPES', 'virtual_machine')),
))),
],
],A computer holds one account's signature per app. Re-pairing from the same computer updates the existing device instead of adding one. Free a computer with php artisan signature:agent-release. See Desktop agent: one signature per computer.
approval | When a user with a paired computer signs a document |
|---|---|
off | The agent is never asked |
prefer (default) | Approve on the computer, with Sign in the browser instead |
enforce | Approval on a paired computer is required, so a user with no paired computer can't sign documents |
server_id and salt must stay stable for an installation. If you rotate APP_KEY, pin them first, or every paired agent will have to pair again. The agent only connects to HTTPS origins.
#Certificate keys (PKCS#12)
An agent can also sign with a key imported from a certificate file, such as a PNPKI .p12. Off by default. See Desktop agent: certificate keys.
'devices' => [
// …
'pkcs12' => [
'enabled' => env('SIGNATURE_AGENT_PKCS12', false),
// DER or PEM files; default: the bundled Philippine Root CA - G2.
'trusted_roots' => [\Kukux\DigitalSignature\Security\AgentCertificateValidator::PNPKI_ROOT],
'revocation' => env('SIGNATURE_AGENT_PKCS12_REVOCATION', 'ocsp_then_crl'), // ocsp_then_crl | crl | off
'unreachable' => env('SIGNATURE_AGENT_PKCS12_UNREACHABLE', 'allow'), // allow | deny
'recheck_ttl' => env('SIGNATURE_AGENT_PKCS12_RECHECK_TTL', 86400), // seconds
'subject_binding' => env('SIGNATURE_AGENT_PKCS12_SUBJECT_BINDING', 'none'), // none | name_match
'one_device_per_certificate' => env('SIGNATURE_AGENT_PKCS12_ONE_DEVICE', true),
],
],#Queue
'queue' => env('SIGNATURE_QUEUE', 'default'),
'queue_connection' => env('SIGNATURE_QUEUE_CONNECTION', null),null uses the application's default connection. Only relevant when SignDocumentAction::make()->queued() is used — the default is synchronous.
#Timestamp Authority (TSA)
Embeds an RFC 3161 trusted timestamp inside the PKCS#7 block. Disabled when url is null.
'tsa' => [
'url' => env('SIGNATURE_TSA_URL', null),
],Free public endpoints:
| Endpoint | Provider |
|---|---|
https://freetsa.org/tsr | FreeTSA |
http://timestamp.digicert.com | DigiCert |
http://tsa.starfieldtech.com | Starfield |
#CRL validation
Checks certificate revocation lists before signing. Disabled by default.
'crl' => [
'enabled' => env('SIGNATURE_CRL_ENABLED', false),
'cache_ttl_hours' => 24,
],Requires the openssl CLI binary in PATH. Self-signed certificates (no CDP extension) are silently skipped.
#Templates and documents
'templates' => [
\App\Pdf\DtrTemplate::class, // a class
'payslip' => ['view' => 'pdf.payslip', 'slots' => ['employee']], // or a Blade template by key
],
'documents' => [
'dtr' => \App\Signatures\DtrDocument::class, // 2.0: documents this app routes
],Class-strings, not closures, so php artisan config:cache keeps working. Both are built through the container on first use. See PDF Templates and Integrating documents.
#Placement designer
| Key | Env | Default | Purpose |
|---|---|---|---|
designer.dpi | SIGNATURE_DESIGNER_DPI | 144 | Resolution of the page previews. Higher is sharper, slower and larger. |
#Stamp: caption and the public page
What is drawn inside each signature's box. It follows COA Circular No. 2021-006, IV.C.13: the handwritten signature with the signatory's full name and the date beside it. Everything is carved out of the placement rectangle; the stamp never grows past the box a signatory or the form gave it.
[signature] Digitally signed
by Juan DelaCruz
Date: 2020.05.21
19:37:33 +08'00'#caption
The text is drawn at a fixed size whatever size the box is, so resizing a placement scales only the signature image.
| Key | Env | Default | Purpose |
|---|---|---|---|
caption.enabled | SIGNATURE_CAPTION_ENABLED | true | Draw the text at all. |
caption.label | SIGNATURE_CAPTION_LABEL | Digitally signed | First line. |
caption.font_pt | SIGNATURE_CAPTION_FONT | 7 | Fixed size of the text. |
caption.min_font_pt | SIGNATURE_CAPTION_MIN_FONT | 4 | Only used when a box is too small for font_pt. The name is never truncated. |
caption.line_height | SIGNATURE_CAPTION_LINE_HEIGHT | 1.15 | Leading. |
caption.gap | SIGNATURE_CAPTION_GAP | 3 | Points between the signature and the text. |
caption.date_format | SIGNATURE_CAPTION_DATE_FORMAT | Y.m.d | PHP format for the Date: line. |
caption.time_format | SIGNATURE_CAPTION_TIME_FORMAT | H:i:s | PHP format for the time line. The UTC offset is appended as +08'00'. |
caption.timezone | SIGNATURE_CAPTION_TIMEZONE | null | Zone the time is shown in. null uses the app timezone; Asia/Manila prints Philippine time. |
caption.color | — | [0, 0, 0] | RGB. |
#verify
| Key | Env | Default | Purpose |
|---|---|---|---|
verify.enabled | SIGNATURE_VERIFY_ENABLED | true | The public GET /signature/verify/{uuid} page. It discloses only what the printed page already shows. |
#Hashing
| Key | Default | Purpose |
|---|---|---|
hash_algo | sha256 | Algorithm for image, document and signed-document hashes. Changing it on a live install makes earlier hashes incomparable; every stored version would report a mismatch. |
#Full environment variable reference
# Drivers
SIGNATURE_CERT_DRIVER=openssl # openssl | cfssl
SIGNATURE_PDF_DRIVER=fpdi # fpdi | tcpdf
# Storage
SIGNATURE_DISK=local # any Laravel disk
# Admin resource
SIGNATURE_RESOURCE_ENABLED=true
SIGNATURE_RESOURCE_ICON=heroicon-o-pencil-square
SIGNATURE_RESOURCE_GROUP= # blank = ungrouped
SIGNATURE_RESOURCE_SORT= # blank = default order
SIGNATURE_RESOURCE_LABEL=Signatures
# Security
SIGNATURE_MACHINE_LOCK=true # reject re-upload from different device
SIGNATURE_DEVICES_ENABLED=true # browser device keys
SIGNATURE_DEVICES_REQUIRE=off # off | warn | enforce
SIGNATURE_DEVICES_USAGE_POLICY=any_registered # or creation_device_only
SIGNATURE_DEVICES_NOTIFY=true # mail the owner when a new device registers
SIGNATURE_AGENT_ENABLED=false # desktop agent pairing + approval
SIGNATURE_AGENT_APPROVAL=prefer # off | prefer | enforce
SIGNATURE_AGENT_MIN_VERSION=0.1.0 # older agents get HTTP 426
SIGNATURE_AGENT_SERVER_ID= # blank = derived from APP_KEY; `signature:install --agent` pins it
SIGNATURE_AGENT_SALT= # same
SIGNATURE_AGENT_BLOCKED_DEVICE_TYPES=virtual_machine # comma-separated; empty = allow VMs
SIGNATURE_AGENT_PKCS12=false # pair agents whose key is an imported certificate (.p12)
SIGNATURE_AGENT_PKCS12_ROOTS= # comma-separated root files; blank = bundled PNPKI root
SIGNATURE_AGENT_PKCS12_REVOCATION=ocsp_then_crl # ocsp_then_crl | crl | off
SIGNATURE_AGENT_PKCS12_UNREACHABLE=allow # allow | deny, when OCSP and CRL are both down
SIGNATURE_AGENT_PKCS12_RECHECK_TTL=86400 # seconds before a signing re-checks revocation
SIGNATURE_AGENT_PKCS12_SUBJECT_BINDING=none # none | name_match
SIGNATURE_AGENT_PKCS12_ONE_DEVICE=true # one active computer per certificate
# Routing, sessions and consent
SIGNATURE_SEQUENCE_MODE=sequential # sequential | parallel
SIGNATURE_MULTI_MODE=progressive # progressive | incremental
SIGNATURE_SESSION_EXPIRY_DAYS= # blank = sessions never expire
SIGNATURE_AUTO_AFFIX_MODE=approval # approval | delegated | implicit
SIGNATURE_ALLOW_IMPLICIT_AFFIX=false
SIGNATURE_AUTO_AFFIX_NOTIFY=true
# Placement designer
SIGNATURE_DESIGNER_DPI=144 # page preview sharpness
# Queue
SIGNATURE_QUEUE=default
SIGNATURE_QUEUE_CONNECTION= # blank = app default
# Floating launcher
SIGNATURE_LAUNCHER_ENABLED=true
SIGNATURE_LAUNCHER_REPLACES_NAV=true
SIGNATURE_LAUNCHER_POSITION=bottom-right
SIGNATURE_LAUNCHER_LABEL=Signatures
SIGNATURE_LAUNCHER_COLOR=
SIGNATURE_LAUNCHER_POLL=60
SIGNATURE_LAUNCHER_WIDTH=56rem
SIGNATURE_LAUNCHER_MANAGE_WIDTH=72rem
SIGNATURE_LAUNCHER_CUSTOMIZABLE=true # placement controls in the super-admin Settings tab
SIGNATURE_LAUNCHER_AVOID_OVERLAP=true
SIGNATURE_LAUNCHER_OFFSET_X=1.5rem
SIGNATURE_LAUNCHER_OFFSET_Y=1.5rem
SIGNATURE_LAUNCHER_GAP=12
SIGNATURE_LAUNCHER_Z_INDEX=40
# Stamp: COA caption, verification page
SIGNATURE_VERIFY_ENABLED=true
SIGNATURE_CAPTION_ENABLED=true
SIGNATURE_CAPTION_LABEL="Digitally signed"
SIGNATURE_CAPTION_FONT=7 # fixed; only the signature scales
SIGNATURE_CAPTION_MIN_FONT=4
SIGNATURE_CAPTION_LINE_HEIGHT=1.15
SIGNATURE_CAPTION_GAP=3
SIGNATURE_CAPTION_DATE_FORMAT="Y.m.d"
SIGNATURE_CAPTION_TIME_FORMAT="H:i:s"
SIGNATURE_CAPTION_TIMEZONE=Asia/Manila
# Optional features
SIGNATURE_TSA_URL= # blank = disabled
SIGNATURE_CRL_ENABLED=false
# CFSSL (only if cert_driver=cfssl)
CFSSL_HOST=http://localhost:8888
CFSSL_PROFILE=client
#Signatory routing, sessions and consent
Added by Signatory Routing. Full explanations of each mode live there; this is the key reference.
#sessions
| Key | Env | Default | Purpose |
|---|---|---|---|
sessions.sequence_mode | SIGNATURE_SEQUENCE_MODE | sequential | sequential honours SlotDefinition::$order (Prepared → Attested → Noted); parallel lets any assigned signatory act at any time |
sessions.expires_after_days | SIGNATURE_SESSION_EXPIRY_DAYS | null | Sessions stop accepting signatures after this many days; null disables expiry |
sessions.notification_channels | — | ['mail'] | Channels for SignatureRequestedNotification, sent to each signatory when it's their turn. [] turns it off. |
#multi_signature
| Key | Env | Default |
|---|---|---|
multi_signature.mode | SIGNATURE_MULTI_MODE | progressive |
progressive— each signature is stamped onto the previous signatory's output and re-signed with that signatory's own certificate. Every visible signature is present and the database holds a verifiable hash chain, but only the most recent PKCS#7 block survives inside the PDF (FPDI rewrites the file on every pass).incremental— true PAdES; requires a driver implementingSupportsIncrementalSigning. Neither bundled driver does, so selecting this mode without one throwsIncrementalSigningUnsupportedExceptionat sign time rather than silently producing a document whose earlier signatures are gone.
#auto_affix
| Key | Env | Default | Purpose |
|---|---|---|---|
auto_affix.mode | SIGNATURE_AUTO_AFFIX_MODE | approval | approval, delegated or implicit |
auto_affix.allow_implicit | SIGNATURE_ALLOW_IMPLICIT_AFFIX | false | Second acknowledgement required before implicit will run |
auto_affix.notify | SIGNATURE_AUTO_AFFIX_NOTIFY | true | Notify the signatory on every auto-affix. Leave this on. |
auto_affix.default_grant_days | SIGNATURE_GRANT_DAYS | 365 | Default lifetime of a delegation grant |
auto_affix.notification_channels | — | ['mail'] | Channels for SignatureAutoAffixedNotification |
approval(default) — never signs on anyone's behalf. The signature is always produced in the signatory's own authenticated request.delegated— signs only where the signatory created a scoped, expiring, revocableSignatureDelegation. A standing grant means the server can produce that user's signature for the grant's lifetime; that is a deliberate trade.implicit— treats being tagged on a record as consent. Unsafe: anyone who can edit the record can then cause that person's certificate to sign it.
Both auto_affix and sequence_mode can be overridden per template with the
auto_affix and sequence_mode keys in the template's config array.
#inbox
| Key | Env | Default |
|---|---|---|
inbox.enabled | SIGNATURE_INBOX_ENABLED | true |
inbox.navigation | SIGNATURE_INBOX_NAV | true |
inbox.navigation_label | SIGNATURE_INBOX_LABEL | Awaiting my signature |
inbox.navigation_icon | SIGNATURE_INBOX_ICON | heroicon-o-inbox-arrow-down |
inbox.navigation_group | SIGNATURE_INBOX_GROUP | null |
inbox.navigation_sort | SIGNATURE_INBOX_SORT | null |
Per-panel override: SignaturePlugin::make()->withoutInbox().
#launcher
The floating button, pinned to a corner of every panel page, that opens a drawer with the documents waiting on the signed-in user and their signature library. On by default.
The drawer is the signing surface, not a notification rail: opening a document
renders the PDF inside it, and the signatory drags their signature onto the
page. width is sized for that — 56rem by default, wide enough to read a
page. It accepts any plain CSS length, never exceeds the viewport, and goes
full-bleed below 640px regardless.
| Key | Env | Default |
|---|---|---|
launcher.enabled | SIGNATURE_LAUNCHER_ENABLED | true |
launcher.replaces_navigation | SIGNATURE_LAUNCHER_REPLACES_NAV | true |
launcher.position | SIGNATURE_LAUNCHER_POSITION | bottom-right |
launcher.icon | SIGNATURE_LAUNCHER_ICON | heroicon-o-pencil-square |
launcher.label | SIGNATURE_LAUNCHER_LABEL | Signatures |
launcher.color | SIGNATURE_LAUNCHER_COLOR | null (built-in neutral) |
launcher.poll_seconds | SIGNATURE_LAUNCHER_POLL | 60 |
launcher.hide_when_empty | SIGNATURE_LAUNCHER_HIDE_WHEN_EMPTY | false |
launcher.width | SIGNATURE_LAUNCHER_WIDTH | 56rem |
launcher.manage_width | SIGNATURE_LAUNCHER_MANAGE_WIDTH | 72rem (drawer width while Manage signatures or All documents I've signed is open) |
launcher.customizable | SIGNATURE_LAUNCHER_CUSTOMIZABLE | true (a super admin can move their own button from the drawer's Settings tab) |
launcher.avoid_overlap | SIGNATURE_LAUNCHER_AVOID_OVERLAP | true |
launcher.offset.x | SIGNATURE_LAUNCHER_OFFSET_X | 1.5rem |
launcher.offset.y | SIGNATURE_LAUNCHER_OFFSET_Y | 1.5rem |
launcher.gap | SIGNATURE_LAUNCHER_GAP | 12 (px) |
launcher.z_index | SIGNATURE_LAUNCHER_Z_INDEX | 40 |
launcher.avoid | — | [] |
launcher.ignore | — | [] |
position accepts bottom-right, bottom-left, top-right, top-left. The
slide-over enters from whichever side the button sits on.
position and offset are the default placement. The drawer's
Settings tab is for super admins only. With customizable on, it lets them
pick their own corner and offsets: the button moves as they choose, and the
choice is saved per user (in digital_signature_user_preferences) and follows
them to other devices. Reset to default goes back to the config. Set
customizable to false to drop the placement controls and apply the config
placement to everyone; saved choices are then ignored, not deleted. The tab
stays for feature access.
#Not landing on the host app's own floating button
A plugin does not own the corner it is dropped into. Host apps put chat
widgets, cookie bars, "back to top" buttons and their own FABs in exactly the
same place, so with avoid_overlap on (the default) the launcher measures what
is already pinned in its corner and stacks itself clear of it:
- It probes the corner on load, on resize, on
livewire:navigated, twice more shortly after load, and whenever something is appended to<body>— chat widgets and cookie bars routinely mount seconds late. - Fixed or sticky, short, and clickable counts as an obstacle: another FAB, a cookie bar, a topbar. It stacks above it, leaving
gappixels. - Tall elements (over 60% of the viewport height) are treated as layout — sidebars, full-height drawers, backdrops. Floating in front of those is the job; stacking above one would push the button off-screen.
pointer-events: nonedecoration, such as a full-width toast rail, is ignored.- A corner crowded past 60% of the viewport height gives up and stays put: drifting into the middle of the page would be worse than the overlap.
Two escape hatches for widgets the detector gets wrong:
'launcher' => [
// Always stack clear of these — for widgets that render into an iframe,
// or mount far too late to be probed.
'avoid' => ['#intercom-launcher', '.crisp-client'],
// Never treat these as obstacles.
'ignore' => ['.my-app-toast-rail'],
],If you already know where the button should go, placing it by hand is better than probing:
'launcher' => [
'position' => 'bottom-left',
'offset' => ['x' => '1.5rem', 'y' => '6rem'], // above the host's FAB
'avoid_overlap' => false,
],offset accepts a plain CSS length (px, rem, em, vh, vw, %);
anything else falls back to the default, since the value lands in a style
attribute. z_index sets the button's layer — the slide-over sits one above,
its backdrop one below — so raise it if a host overlay covers the button, and
lower it if the button covers something that matters more.
replaces_navigation is the key worth understanding. While the launcher is
on, the inbox page and the Signatures resource stop registering sidebar/topbar
items — the launcher is the entry point, and two doors to one room is clutter.
Both pages stay fully routable and the slide-over links to them. Set it to
false to have the launcher and the navigation items.
Turning the launcher off restores both navigation items automatically: suppression is conditional on there being a launcher to replace them with, so no combination of these flags can leave a panel with no way to reach signatures.
color is a plain hex rather than a Filament color token, because the CSS
custom-property format for those changed between Filament majors and a button
that renders invisible on one of the three supported versions would be worse
than one that isn't brand-coloured by default.
Per-panel override: SignaturePlugin::make()->withoutFloatingLauncher().
#feature_access
A super admin can limit a feature to selected people from the drawer's Settings tab while it's being tested. See Feature access.
| Key | Default | |
|---|---|---|
feature_access.search_columns | ['name', 'email'] | Users-table columns searched when adding people. A column your table doesn't have gives no results. |
#filament_version
| Key | Env | Default |
|---|---|---|
filament_version | SIGNATURE_FILAMENT_VERSION | auto-detected |
Normally read from Composer's installed-versions manifest — a class probe
cannot tell Filament 4 from 5, since both ship Filament\Schemas\Schema. Set
this only to force a branch (3, 4 or 5), e.g. to exercise the v3 component
classes on a v5 install.