Summary
A checklist of smaller hardening gaps found while auditing feature/126-implement-peppol, each individually small but worth tracking rather than losing.
1. Qonto's settings() declares an optional key as mandatory
Modules/Invoices/Peppol/Clients/Qonto/QontoClient::settings() returns ['access_token', 'staging_token']. QontoProvider doesn't override authenticate(), so it falls through to BaseProvider::authenticate() (Modules/Invoices/Peppol/Providers/BaseProvider.php:106-117), which loops over every key settings() returns and fails if any is empty — treating both as equally mandatory. But staging_token is used only conditionally (QontoClient::getAuthenticationHeaders(): if ($this->stagingToken) { ... }) — it's an optional sandbox header, not a real auth requirement. Net effect: a fully and correctly configured production Qonto integration (real access_token, no staging_token) would report authenticate() === false. Compare with EInvoiceBeProvider (['api_key']) and StorecoveProvider (['api_key', 'legal_entity_id']), where every listed key genuinely is unconditionally required — Qonto is the one sibling that drifted from that pattern. Currently inert (nothing calls ->authenticate() on any provider yet outside the class hierarchy itself), but part of the public ProviderInterface contract, so it's a landmine for whenever something starts calling it.
Fix shape: decide how ProviderInterface should express "required vs. optional" settings (e.g. a second optionalSettings() method, or a keyed array with per-key required flags) and update BaseProvider::authenticate()'s default accordingly.
2. Request/response logger's secret redaction has a gap
The request/response logger's secret redaction misses the request payload and response body — dormant today (the feature flag is off by default, never enabled in production code), but would leak plaintext client_secret/access_token the moment it's switched on for debugging.
Fix shape: extend the redaction to cover the full request payload and response body, not just whatever fields it currently covers.
3. FormDbConstraintAuditTest has a blind spot: it can't see a NOT-NULL column missing from a form entirely
Modules/Core/Tests/Feature/FormDbConstraintAuditTest.php only iterates fields that are present in the resolved form schema and checks whether their rules are strict enough — it never walks the DB schema the other way to ask "is every NOT-NULL/no-default column represented by some form field at all?" This is exactly why it stayed green despite merchant_clients.client_id being NOT NULL with no form field (fixed in the PR this issue was filed from, but the audit tool itself still can't catch the next occurrence of this bug class).
Fix shape: extend auditResource() with a reverse pass — for each NOT-NULL, no-default, non-autoincrement column not in {id, created_at, updated_at, deleted_at}, assert it has a corresponding entry in the form's fields, subject to the same FormDbGapKnownExceptions::KNOWN_GAPS allowlist.
4. No cron/scheduler runner exists in the dev or deploy stack
bootstrap/app.php now has a withSchedule() callback registering peppol:poll-status (every 15 min) and peppol:retry-failed (every minute) — but nothing in the project's docker stack (ivpldock-workspace-1 and friends) or deploy configuration actually runs php artisan schedule:run on a timer. Confirmed: no cron container, no supervisor entry, no /etc/cron.d/* beyond the stock PHP session-cleanup job. Laravel's scheduler is inert without something invoking schedule:run every minute.
Fix shape: add a cron entry or supervisor process to the deploy/dev stack that runs php artisan schedule:run every minute (commonly a dedicated lightweight scheduler container, or a cron line in the existing php-fpm/workspace image).
Source
Found by mind-the-gap and release-resilience audits of feature/126-implement-peppol vs develop on 2026-09-12.
Summary
A checklist of smaller hardening gaps found while auditing
feature/126-implement-peppol, each individually small but worth tracking rather than losing.1. Qonto's
settings()declares an optional key as mandatoryModules/Invoices/Peppol/Clients/Qonto/QontoClient::settings()returns['access_token', 'staging_token'].QontoProviderdoesn't overrideauthenticate(), so it falls through toBaseProvider::authenticate()(Modules/Invoices/Peppol/Providers/BaseProvider.php:106-117), which loops over every keysettings()returns and fails if any is empty — treating both as equally mandatory. Butstaging_tokenis used only conditionally (QontoClient::getAuthenticationHeaders():if ($this->stagingToken) { ... }) — it's an optional sandbox header, not a real auth requirement. Net effect: a fully and correctly configured production Qonto integration (realaccess_token, nostaging_token) would reportauthenticate() === false. Compare withEInvoiceBeProvider(['api_key']) andStorecoveProvider(['api_key', 'legal_entity_id']), where every listed key genuinely is unconditionally required — Qonto is the one sibling that drifted from that pattern. Currently inert (nothing calls->authenticate()on any provider yet outside the class hierarchy itself), but part of the publicProviderInterfacecontract, so it's a landmine for whenever something starts calling it.Fix shape: decide how
ProviderInterfaceshould express "required vs. optional" settings (e.g. a secondoptionalSettings()method, or a keyed array with per-key required flags) and updateBaseProvider::authenticate()'s default accordingly.2. Request/response logger's secret redaction has a gap
The request/response logger's secret redaction misses the request payload and response body — dormant today (the feature flag is off by default, never enabled in production code), but would leak plaintext
client_secret/access_tokenthe moment it's switched on for debugging.Fix shape: extend the redaction to cover the full request payload and response body, not just whatever fields it currently covers.
3.
FormDbConstraintAuditTesthas a blind spot: it can't see a NOT-NULL column missing from a form entirelyModules/Core/Tests/Feature/FormDbConstraintAuditTest.phponly iterates fields that are present in the resolved form schema and checks whether their rules are strict enough — it never walks the DB schema the other way to ask "is every NOT-NULL/no-default column represented by some form field at all?" This is exactly why it stayed green despitemerchant_clients.client_idbeing NOT NULL with no form field (fixed in the PR this issue was filed from, but the audit tool itself still can't catch the next occurrence of this bug class).Fix shape: extend
auditResource()with a reverse pass — for each NOT-NULL, no-default, non-autoincrement column not in{id, created_at, updated_at, deleted_at}, assert it has a corresponding entry in the form's fields, subject to the sameFormDbGapKnownExceptions::KNOWN_GAPSallowlist.4. No cron/scheduler runner exists in the dev or deploy stack
bootstrap/app.phpnow has awithSchedule()callback registeringpeppol:poll-status(every 15 min) andpeppol:retry-failed(every minute) — but nothing in the project's docker stack (ivpldock-workspace-1and friends) or deploy configuration actually runsphp artisan schedule:runon a timer. Confirmed: no cron container, no supervisor entry, no/etc/cron.d/*beyond the stock PHP session-cleanup job. Laravel's scheduler is inert without something invokingschedule:runevery minute.Fix shape: add a cron entry or supervisor process to the deploy/dev stack that runs
php artisan schedule:runevery minute (commonly a dedicated lightweightschedulercontainer, or a cron line in the existing php-fpm/workspace image).Source
Found by
mind-the-gapandrelease-resilienceaudits offeature/126-implement-peppolvsdevelopon 2026-09-12.