Repository navigation
1.9.0
Fixed — the flat driver leaked webhook credentials between brands
It had no brand concept at all. FileStore was a singleton bound to one path and nothing under Repositories/FlatFile read or wrote a brand, so content/webhooks/ held one undifferentiated set and every brand read every brand's hooks. The eloquent driver scoped correctly the whole time, so the same install isolated or did not depending on a value that reads like a storage preference.
This is worse here than in a CRM. A webhook config carries a destination URL and the credentials it authenticates with. A cross-brand read is not "one tenant sees another's data" — it hands over a bearer token. A test in FlatDriverBrandIsolationTest asserts precisely that, and it failed before this release:
Failed asserting that '…"auth_config":{"token":"super-secret-token"}…'
does not contain "super-secret-token".
Firing a hook from the wrong brand also posts one tenant's payload to another tenant's endpoint.
Brands now live in the path:
content/webhooks/{brand}/outbound/{handle}.yaml
content/webhooks/{brand}/inbound/{handle}.yaml
content/webhooks/{brand}/rules/{handle}.yaml
content/webhooks/{brand}/templates/{handle}.yaml
A brand: key inside each YAML was rejected: the handle is the filename, so a key would give every definition two identities that can disagree, and a missing or misspelt one would fall through to the default brand — a leak that reads like a typo. With a directory the isolation is structural and visible in ls.
The semantics live in one class, Storage\BrandSegments, matching statamic-marketing and statamic-leadhub so all three flat drivers behave the same way.
- [fix] Single-brand installs change nothing. No directory, no move, nothing to run.
- [fix] The pre-brand layout is read as the default brand's, and only that brand's, so an install that enables multi-brand does not lose its hooks.
- [fix] Fail closed. Multi-brand with no current brand reads nothing rather than everything, matching the eloquent global scope.
- [fix] A write lands where the definition already lives, so an un-migrated install gains no second copy that shadows the original.
Added — webhook-manager:migrate-flat-brands
Moves the pre-brand layout into a brand directory.
php artisan webhook-manager:migrate-flat-brands --dry-run
php artisan webhook-manager:migrate-flat-brands
php artisan webhook-manager:migrate-flat-brands --brand=acmeOnly ever moves. Never overwrites — a target that already exists means a finished migration or a genuine conflict, and neither is resolved by clobbering — never deletes, and a second run is a no-op.
Notes
Segment resolution is memoised per brand identity; it is consulted on every file operation and the key carries the brand, so a BrandContext::runFor() switch inside one process invalidates it.
tests/Feature/FlatDriverBrandIsolationTest.php and MigrateFlatBrandsCommandTest.php cover this. Five of the six isolation cases failed before the change; the sixth is the single-brand case, which must not change and does not.
Found by auditing all three flat drivers after the same defect was fixed in
statamic-leadhub1.11.0.statamic-marketinghas been correct since 1.6 and needed nothing.