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.
Scope of this assessment. No prior assessment exists; this evaluation is derived from the 3.2.0 release notes, ADR 0041, and the linked documentation tree. Confidence is moderate on architecture and high on the release's stated behaviour; claims about internals (logging, certificate lifecycle handling) are inferred and flagged as open questions.
Architecture fit. The package is a Laravel-native A1 (PKCS#12) PDF signer for the ICP-Brasil context. 3.2.0 extends it from "a library you call" to "a capability an agent can invoke," and — correctly — resolves the authorization question through the framework primitive Laravel teams already use: Gate::define. Placing policy in the Gate rather than in package config means existing team conventions (policies, role packages, tenant scoping) apply without a parallel authorization model. That is the right architectural bet.
Integration feasibility. Defining an ability is three lines per the notes. The real integration work is not code volume but decisions: who may read, who may sign, and whether an LLM-initiated signature is permitted at all without a human in the loop. The package provides the approval flow and gate hook; it does not (and should not) decide the policy.
Technical risk.
| Risk | Severity | Note |
|---|---|---|
| Fail-open authorization default | High | Undefined ability ⇒ allow. Safe for upgrades, dangerous for greenfield — an operator who forgets Ability::Sign ships an unrestricted signing agent. |
Silent max_bytes default on upgrade |
Medium | 50 MB applies even to configs published under 3.1.0. Workflows signing large scanned contracts break without a config diff. |
| Agent-initiated signature as a material action | High | The model selects the document; the app holds the certificate. Approval flow mitigates, but the control is procedural, not cryptographic. |
| Metadata-based size check | Low–Med | Size validated from disk metadata before load; TOCTOU if the object changes between stat and read. |
| MCP OAuth surface | Medium | Mcp::oauthRoutes() for remote clients widens the attack surface; throttling is documented but not enforced. |
Positive signals: 233 tests, PHPStan at max, 100% type coverage, suite green with both optional SDKs removed (clean optional-dependency boundary), and a real-model end-to-end scenario covering refusals and approval. ADR 0041 shows the decision is recorded, not incidental.
Key questions.
max_bytes check re-validated against actual bytes read, closing the TOCTOU window?Stack fit. Standard Laravel: service provider, publishable config, Storage disks, openssl extension. The agent layer is opt-in and depends on laravel/mcp and laravel/ai; the package degrades cleanly without them, which is a meaningful procurement point — non-AI workloads take no runtime dependency.
Migration path. 3.1.0 → 3.2.0 is documented in UPGRADE.md. No breaking changes are declared, but two behaviour deltas land without an opt-in: the 50 MB max_bytes ceiling, and gate-backed authorization that defaults to allow. Treat this as a minor version with migration chores, not a drop-in patch.
Compatibility. Verify Laravel/PHP matrix against our platform version and confirm both optional SDKs are pinned to versions the package tested against. The "suite with both SDKs removed" claim should be re-verified in our CI, not assumed.
Sequencing.
agents.max_bytes explicitly (or null to preserve 3.1.0 behaviour) — do this before enabling agent tools.Ability::Read and Ability::Sign gates in a policy class with tests asserting refusal paths, not just the happy path.Mcp::oauthRoutes() unless remote clients are a confirmed requirement.Sign in production, and consider requiring approval unconditionally for that ability.ToolSearch grouping as a later optimization — the notes correctly argue it adds nothing at two tools.Maintenance. Release hygiene is strong: changelog, upgrade guide, ADRs, versioned docs built from the tag. Ongoing cost to us is low — a config key and a policy class — provided we pin the minor version and read upgrade notes each release given the fail-open default.
Support. Primary dependency risk is maintainer concentration. Mitigation: the package is small, MIT-licensed, and the signing path is thin over openssl; a fork or in-house wrapper is a credible fallback. Budget for that contingency rather than assuming upstream responsiveness.
Scaling. Signing is CPU-bound and, per the notes, the document is held in memory by the engine — so concurrency × max_bytes is the real ceiling. At the 50 MB default, ten concurrent signatures is a 500 MB working set before PHP overhead. Recommend: run signing on a queue with a bounded worker pool, set max_bytes to the largest document we actually accept (not the default), and separate the agent/MCP tier from the signing workers.
Failure modes to instrument.
Ramp-up. Gate/Policy usage is routine for Laravel engineers (hours). MCP and agent-tool concepts are new to most teams (days), and the approval-flow semantics — including "approval is still asked for a call the gate will refuse, and the person is told it will be refused" — need to be understood before anyone configures it, or reviewers will approve reflexively. ICP-Brasil compliance obligations (evidence retention, certificate custody) sit outside the package and require a compliance owner, not just an engineer.
Recommendation. Adopt, gated on four conditions: explicit gate definitions with refusal tests, an explicit max_bytes value, queued signing with a sized worker pool, and mandatory human approval on Ability::Sign until an audit trail is confirmed. The authorization model is sound; the defaults are convenience-oriented and must be overridden deliberately.
How can I help you explore Laravel packages today?