Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel A1 Pdf Sign Laravel Package

lsnepomuceno/laravel-a1-pdf-sign

A Laravel package for digitally signing PDF documents with A1 certificates. It integrates easily into your app to apply ICP-Brasil compliant signatures, add visual signature stamps, and protect files with a practical, fluent API.

View on GitHub
Deep Wiki
Context7

Getting Started

Version 3.2.0 is an incremental release on top of the 3.1.0 agent tooling: it does not change the core signing API. If you have never used the package, the first productive path is still signing a PDF with an A1 certificate; the agent tools are a second step.

composer require lsnepomuceno/laravel-a1-pdf-sign
php artisan vendor:publish --tag=a1-pdf-sign-config
  1. Point the config (or env) at your .pfx/.p12 certificate and its password, then sign a PDF in a test to confirm the certificate, password and PDF engine all work. Do this before touching anything agent-related — it isolates certificate problems from MCP problems.
  2. Read docs/decisions/0041-agents-are-authorised-per-document.md first. It is the design rationale for the whole 3.2.0 release and explains why the gate is consulted before the file is opened.
  3. If you are coming from 3.1.0, read UPGRADE.md#from-310-to-320. The only behaviour you get without editing anything is the new 50 MB agents.max_bytes cap.
  4. Then open the agent guide at https://lsnepomuceno.github.io/laravel-a1-pdf-sign/guide/agents, which covers throttling the MCP route and Mcp::oauthRoutes() for remote clients.

A good first use case is a single-tenant app where every authenticated user may read and sign everything: define both abilities to return true, set agents.max_bytes to something sane for your documents, and move on. The second use case — a shared contracts disk where users must only see their own files — is what this release is really for, and is worth doing early because it forces you to think about path ownership before the tool is in production.

Implementation Patterns

Define both abilities explicitly, in one place. The gate is the only thing standing between a model-chosen path and a customer's contract, and an undefined ability allows. Register Read and Sign together (a dedicated service provider or a AgentAbilities class) so a reader can see the whole policy at once:

use LSNepomuceno\LaravelA1PdfSign\Agents\Ability;

Gate::define(Ability::Read->value, function (User $user, string $disk, string $path) {
    return $disk === 'contracts' && $user->ownsContractPath($path);
});

Gate::define(Ability::Sign->value, function (User $user, string $disk, string $path, string $destinationDisk, string $destinationPath) {
    return $disk === 'contracts'
        && $user->ownsContractPath($path)
        && $destinationDisk === 'contracts'
        && str_starts_with($destinationPath, "signed/{$user->tenant_id}/");
});

The two signatures are deliberately different: Sign also receives the destination disk and path, because writing a signed artefact into another tenant's folder is as bad as reading their source document. Treat the destination check as mandatory, not decorative.

Normalize paths before comparing. The gate receives the strings the model produced. Reject .. segments and leading slashes, and resolve ownership by lookup rather than by string prefix alone — str_starts_with($path, "tenants/{$id}/") will not save you from tenants/1/../2/file.pdf.

Keep the gate cheap. It is consulted per call and receives strings, not models, so every check is a query unless you cache. Resolve the user's contract set or tenant prefix once per request (a memoised property on the user, or a request-scoped cache) rather than running a where for each ability evaluation.

Let the approval flow be the second gate, not the only one. Signing still prompts for human approval, and approval is requested even for a call the gate will refuse — the person is told it will be refused. That means the gate is your security boundary and approval is your intent boundary; do not use approval prompts as authorisation, and do not assume a refused call is silent.

Fail closed inside the gate. Return false for every unhandled case, and do not let an exception escape a gate closure expecting it to be interpreted as a denial. A true from a Gate::before callback bypasses per-document rules entirely, so do not add a global before for convenience.

Tune agents.max_bytes per deployment. 50 MB is generous for text contracts and tight for scanned annexes. Raise it if your documents legitimately exceed it; set it to null only if you understand that this restores 3.1.0's unbounded behaviour and lets a model name a multi-gigabyte file that the engine will hold in memory.

Throttle the MCP route and authenticate it. An unauthenticated MCP endpoint is an unauthenticated agent endpoint. Put your normal auth middleware plus a rate limit on it, and use Mcp::oauthRoutes() if remote clients (rather than your own app) are the consumers.

Leave ToolSearch alone unless you have grown the toolset. The package's own server exposes two tools and deliberately does not group them behind laravel/mcp's ToolSearch: at that size it saves nothing, and execute_tools would hide the read-only annotations the model uses to reason about what is safe to call. Adopt ToolSearch only if you have added many tools of your own to the same server.

Test the refusal path, not just the happy path. Gate::fake() or explicit definitions plus Storage::fake('contracts') gives you a fast feature test for the cross-tenant case. Assert that a refused path returns the refusal rather than a "file not found", since the design guarantees the gate is consulted before the document is looked at.

Gotchas and Tips

  • Undefined ability means allow. This is the release's stated compatibility choice, and it is also the easiest way to ship an open agent. If you do nothing else after upgrading, define both abilities — even as return true — so that the policy exists and the next developer extends it instead of discovering it.
  • The 50 MB cap applies whether or not you published the config. A config published under 3.1.0 has no max_bytes key, and a missing key means the default. You cannot opt out by doing nothing; you opt out by setting the key to null. Watch for this if you have large scanned PDFs that used to go through.
  • The size check reads disk metadata. It is cheap because it does not load the file, but that also means it depends on the driver reporting a size. If a file's size is unknown to the disk, do not assume the limit was enforced — test against your actual driver (S3, SFTP, local) rather than only Storage::fake.
  • Users get an approval prompt for a call that will be refused. This is intentional and the reasoning is in decision 0041, but it reads as a bug to end users. If the prompt is user-facing, surface the same ability check in your own UI before invoking the tool so the refusal happens where the user can understand it.
  • Read and Sign are separate policies with separate signatures. A permissive Read closure does not constrain Sign in any way, and a Sign closure that ignores its destination arguments will happily let a signed file land in someone else's directory.
  • The gate receives a disk name and a path, not a model. Ownership must be resolved by query. If you find yourself matching on filenames, you have probably picked the wrong identifier — store an owner or tenant segment in the path and check that.
  • Agent tooling is optional. The suite is verified with both SDKs removed, so the agent features depend on optional packages being present; core signing is unaffected. If Ability is not resolvable in your app, check your installed optional dependencies before debugging the gate.
  • Debugging gate wiring: Gate::abilities() shows what is registered; a mistyped ability string silently falls back to allow. Using Ability::Read->value / Ability::Sign->value rather than literals removes that class of failure.
  • Upgrading from 3.1.0 is safe by default but is not a no-op. Nothing changes for authorisation (no gates defined, everything allowed) and one thing changes for everyone (the size cap). Read the upgrade guide before concluding that 3.2.0 "did nothing" in staging.
  • The release is verified at 233 tests, PHPStan level max and 100% type coverage, which is a good signal about the package's internals — it says nothing about your gate closures, which are the part that decides whether the feature is worth anything.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib