Skip to content

Resolve licence decisions through the signed marketplace client - #726

Merged
howdu merged 2 commits into
mainfrom
fix/marketplace-entitlements-contract
Oct 2, 2026
Merged

howdu merged 2 commits into
mainfrom
fix/marketplace-entitlements-contract

Conversation

@howdu

@howdu howdu commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

Core resolved the marketplace client from the container under a key that nothing bound. On an installed site the lookup missed and licence decisions fell back to an unsigned request, which the marketplace rejects with 401 (it requires an instance signature). The same fallback gated on instance id and webhook secret being present in config, so a site connected only through the database never signed either. The signed client also dropped the domain the marketplace requires, so fixing the binding alone would have turned the 401 into a 422.

Change

  • Core depends on a new ExtensionEntitlements contract (licenceDecision, verifyActivation) with a NullExtensionEntitlements default, bound with bindIf.
  • The marketplace package binds MarketplaceExtensionEntitlements, which uses the signed client and checks receipt shape before verifying the signature. The receipt-shape rules moved from Core into the marketplace implementation; the runtime gate keeps its catch-all so outcomes are unchanged.
  • The unused capell.marketplace.activation-verifier string binding is removed.
  • The signed client now sends domain, using the caller's domain or this site's host when none is given (feedback has none).

Tests

  • Signed-client regression test: the fake returns 401 without instance headers and 422 without a domain; it failed on the old code and fails with the domain line removed.
  • Arch guard that fails on any container lookup, bind or bound() of a capell.marketplace. string; shown to fail against the pre-change files.
  • Seven real-signature receipt cases (valid, tampered, revoked, non-perpetual, wrong extension, version 2, missing field).
  • Locally: packages/marketplace/tests, packages/core/tests/Arch, packages/core/tests/Unit/PackageRegistry, packages/core/tests/Feature/Marketplace — 1017 passed. Pint, Rector, PHPStan clean on changed files. Not run: the full Core suite.

Behaviour changes

With no credentials at all the error text changes from "Marketplace could not resolve this licence decision." to the client's "Marketplace instance is not registered…"; both are RuntimeExceptions and the callers handle them as before.

Core looked the marketplace client up under a container key nothing bound,
so installed sites fell back to an unsigned request the marketplace rejects
with 401. Core now depends on an ExtensionEntitlements contract with a Null
default; the marketplace package binds the signed implementation. The signed
client also sent no domain, which the marketplace requires, so it now sends
the caller's domain or this site's host.

A guard test fails on any container lookup of an unbound marketplace key.
@howdu
howdu enabled auto-merge (squash) October 2, 2026 16:21
@howdu
howdu merged commit fe845c0 into main Oct 2, 2026
31 checks passed
@howdu
howdu deleted the fix/marketplace-entitlements-contract branch October 2, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant