All notable changes to angeo/module-ucp are documented here.
Format follows Keep a Changelog.
Versioning follows Semantic Versioning.
Documentation only. The profile does not change.
- README: which module serves which capability — fulfillment
(
angeo/module-ucp-checkout1.0+), order with webhooks (1.1+) and the MCP checkout tools (angeo/module-ucp-checkout-mcp).
- Default REST endpoint pointed at nothing. With REST Endpoint URL
empty, the profile advertised
{baseUrl}/rest/V1/ucp. Magento's Web API has no such route and the Angeo REST modules answer under/ucp/v1, so an agent that followed the profile got 404 on every call. The default is now{baseUrl}/ucp/v1(Config::DEFAULT_REST_PATH). A configured URL is used as before.
- Capabilities that need endpoints are advertised only when served.
Cart, checkout, order and the fulfillment, discount and buyer-consent
extensions now need both the admin toggle and a module that registers
an implementation (
implementationsargument ofConfig, set in that module'sdi.xml). Before, switching on Checkout advertised a capability no module served. Catalog and permalink are not gated.angeo/module-ucp-checkoutregisters checkout and discount. angeo:ucp:validatewarns about toggles that are on but have no implementation.
Config::isImplemented(),Config::getUnimplementedToggles(),Config::GATED_CAPABILITIES,Config::DEFAULT_REST_PATH.
A code-quality release. The published profile does not change.
- GitHub Actions CI (
.github/workflows/ci.yml) on PHP 8.1–8.5: composer validate, PHP lint, Magento2 coding standard, PHPUnit. PHPStan runs once, in its own job on PHP 8.2, as in the Mage-OS modules. Magento packages come from the Mage-OS mirror. phpstan.neon(level 5,bitexpert/phpstan-magento). The extension generates Magento factory classes during analysis, so no stub files are needed.- Dev dependencies:
magento/magento-coding-standard,phpstan/phpstan^2,bitexpert/phpstan-magento.
composer.json: PHP 8.1–8.5 (was>=8.2). The code uses no PHP 8.2+ syntax or functions; CI now runs on 8.1 too. Magento dependencies have bounded ranges (Magento 2.4.6 – 2.4.x). Dev dependencies moved to PHPUnit 10.5 and PHPStan 2.phpstan.neonreplaced with the suite-wide config (level 5). The old one ignored every undefined Magento method.AuthorityBinding,Spec,StructuredFieldsandVerificationResultare no longerfinal(Magento coding standard).ProfileFetcher: DNS lookup no longer uses the@operator; a failed lookup still yields no addresses.ProfileFetcherno longer callscurl_close()(deprecated in PHP 8.5).angeo:ucp:validate: removed anif (true)wrapper and a deadis_array()check. Same output.Controller\Router: removed the unusedTARGET_ACTIONconstant.- Tests:
RawResultSpymoved to its own file; test classes are no longerfinal.phpunit.xmlno longer lists the non-existentPlugindirectory.
- Unit tests: Store mocks use
Magento\Store\Model\Store(forgetBaseUrl()); two expectations updated to the current JWK warning text and thekeysarray, one of which contradicted itself.
- README: one badge row across the suite — CI, Packagist version and downloads, PHP 8.1 – 8.5, supported Magento range, Mage-OS Extension Directory, license.
- Magento2 coding standard: 0 errors. PHPStan level 5: no errors.
Adds the piece 2.0.0 published keys for but never used: verification of inbound RFC 9421 signatures. Also fixes a discovery-cache defect found while reviewing an external critique of the module.
-
Api\SignatureVerifierInterface— verification of inbound RFC 9421 signatures, implementing the spec's identity-resolution algorithm. It lives here rather than in an endpoint module because every UCP endpoint needs the same logic and key resolution runs through the profile machinery this module already owns.angeo/module-ucp-catalog2.1.0 consumes it.Why this and not response signing first: the spec's MUST runs in the inbound direction — a business MUST reject a request whose counterparty profile cannot be fetched or fails validation. Until 2.1.0 the endpoints advertised by this profile answered anyone who sent a POST. Signing our own responses is still to come, and is the weaker obligation of the two.
What is implemented:
- Signature base reconstruction (
Model\Signature\SignatureBase) including the derived components@method,@authority,@path,@query,@scheme,@target-uri, and RFC 9421 §2.1.2 dictionary-member selection sosignature-agent;key="sig1"can be covered. ecdsa-p256-sha256,ecdsa-p384-sha384anded25519. ECDSA signatures arrive as rawr||sand are converted to DER for OpenSSL; the key'skty/crvmust match the nominatedalg, so a signer cannot downgrade to a weaker algorithm than the profile advertises.- Covered-component enforcement. A signature that does not cover the
request target, the body digest when a body is present, or a present
ucp-agent/signature-agent/idempotency-keyheader is skipped — an uncovered component is an unsigned component. - Content-Digest is checked against the body, not merely required to be covered. Covering a digest header that nobody validates authenticates nothing.
- Multiple signatures per request: each is attempted independently and the request is authenticated when at least one verifies, per RFC 9421 §4.3.
- For
tag="web-bot-auth",keyidMUST equal the RFC 7638 thumbprint of the matched JWK — the binding 2.0.0's thumbprint kids exist for. - Keys marked
use: "enc", or withkey_opslacking"verify", are skipped during resolution (RFC 7517 §4.2/§4.3). createdin the future andexpiresin the past are rejected, with a 300-second skew allowance.
- Signature base reconstruction (
-
Model\Signature\ProfileFetcher— implements the spec's Fetching rules, which are normative precisely because this class dereferences a URL supplied by the request and is an SSRF primitive otherwise: HTTPS only, no redirect following, resolved-address checks against RFC 6890 special-use ranges (including the cloud metadata address169.254.169.254) so DNS rebinding does not slip through, a 128 KiB body bound, connect and response timeouts, and a 60-second cache TTL floor. On an unknownkidthe profile is force-refreshed once — a counterparty that just rotated should not be locked out for a whole TTL — rate-limited to once per origin per TTL floor. -
Admin setting:
Request Security → Inbound Signature Verification, with three modes.disabled(default) — 2.0.x behaviour, everything served unverified.optional— the migration mode: a counterparty that signs is authenticated, one that does not is still served. An invalid signature is rejected here too; whatoptionalrelaxes is only the absence of one.required— unsigned requests get 401.
Default is
disabledon purpose. No agent signs requests to a store that has never advertised signature support, so defaulting torequiredwould 401 every existing integration on upgrade. An unrecognised stored value also falls back todisabledrather thanrequired— a configuration typo should not take a store's UCP endpoints offline.
-
Discovery-cache fragmentation. The 2.0.0 router normalised trailing slashes before matching, so
/.well-known/ucpand/.well-known/ucp/both served the profile. Two URLs returning identical bytes are two CDN cache entries, halving the hit rate on the single most-fetched document the module serves. 2.1.0 serves the canonical path only and issues a 301 for the trailing-slash form, preserving the query string.For the record, the related claim that the module also serves
/.well-known/ucp.jsonis not accurate: no such route exists in the module, and no such path exists anywhere in the specification at tagv2026-08-25. Only the trailing-slash variant was real.
ext-curlis now a hard requirement rather than assumed. Verification cannot resolve a counterparty's keys without it, and failing at install time is better than failing per-request.
dev/signature-verification/verify_test.phpruns the verifier against a real Ed25519 keypair with no Magento or network dependency. Thirteen cases, including: tampered body, swapped@path, a query string appended after signing, a signature omittingcontent-digestfrom its covered set, a presentidempotency-keyleft uncovered,algsubstitution, a key markeduse: "enc", an unknownkid, andSignature-Inputsent withoutSignature.
Adopts UCP spec release 2026-08-25, published on 25 August 2026 — the
first new dated release since 2026-04-08. This is a major version because the
profile's key field changed name and the advertised protocol version changed;
both are visible to every platform that fetches /.well-known/ucp.
Everything below was verified against Universal-Commerce-Protocol/ucp at
tag v2026-08-25, and the profiles this module generates are validated in CI
against that tag's official JSON Schemas.
1.4.0 was not broken. Its profile was, and still is, a valid
2026-04-08 business profile — signing_keys was the correct field for that
release, and the spec explicitly allows a business to keep serving an older
version. Staying on 1.4.0 is a legitimate choice.
What 1.4.0 cannot do is advertise 2026-08-25. The two releases disagree
about where keys live and where capability documentation lives, so adopting
the new version means adopting all of it at once. That is what this release
does.
-
Protocol version is now
2026-08-25(Config::PROTOCOL_VERSION). The previous value is retained asConfig::PREVIOUS_PROTOCOL_VERSIONfor migration tooling. -
Signing keys moved from
signing_keysto the top-levelkeys[]array. Perschemas/profile.json#/$defs/baseat the new tag,keys[]is the canonical field and "this is where every UCP verifier reads them". It is an RFC 7517 JWK Set, which makes the profile document simultaneously a valid JWK Set that a signer can reuse as its Web Bot Auth key source. A profile advertising2026-08-25while still usingsigning_keyspublishes keys nothing will read —angeo:ucp:validatenow reports that as an error. No configuration change is needed: the stored JWKs are re-published under the new field automatically. -
All capability
specURLs moved. The documentation tree was reorganised: catalog, cart, checkout and order now live under/specification/shopping/, and fulfillment, discount and buyer_consent under/specification/shopping/extensions/. Every URL the profile emits is now centralised in the newModel\Specclass rather than assembled from string fragments in three different files. -
Config::getPublicSigningKeys()is deprecated in favour ofgetPublicKeys(). The old method still works and forwards to the new one. -
Reverse-domain pattern widened to the pattern in
schemas/common/types/reverse_domain_name.jsonat the new tag. The 1.x pattern rejected names the spec lists as valid — interior hyphens (com.example-shop.checkout), leading digits after the first segment (com.2example.cart), and punycode TLDs (xn--p1ai.example.checkout). A payment handler or third-party service binding using any of those was silently dropped from the profile under 1.x.
-
Model\AuthorityBinding— the spec's authority-binding derivation algorithm, implemented as specified rather than as a string prefix test. 2026-08-25 makes this a MUST: a platform validates every declaredschemaURL and rejects the entity outright when the URL's label-reversed host is not the entity name or a label-aligned prefix of it. A capability that fails is treated as not present and never activated, silently. The implementation handles the cases the spec calls out explicitly — userinfo decoys (https://ucp.dev@evil.example/x.jsonhas hostevil.example), IP-literal and single-label hosts, and near-miss namespaces (com.examplecorpvscom.example).angeo:ucp:validateruns it over every capability, service binding and payment handler. -
Ed25519 (OKP) and P-384 signing keys.
angeo:ucp:keys:generate --type=ed25519|es384|es256. The spec RECOMMENDS Ed25519 for signers opting into Web Bot Auth interop on HTTP transport; ES256 remains the universal baseline and is what AP2 mandate signing requires, so the command points that out when you generate an Ed25519 key alone. Ed25519 uses ext-sodium (bundled with PHP 7.2+) and degrades with a clear message when it is unavailable — it is asuggest, not a hard requirement. -
RFC 7638 JWK thumbprint as the default
kid. The spec REQUIRES the thumbprint form for any key used in dual-audience Web Bot Auth signatures, so thatUCP-AgentandSignature-Agentlookups resolve the same key. The 1.xangeo-ucp-YYYY-xxxxxxxxkid quietly broke that.--kidoverrides. The implementation is checked against the spec's own published example key. -
Zero-downtime key rotation.
angeo:ucp:keys:generate --addappends tokeys[]instead of replacing it, so a new key can be published, signing moved over, and only then the old entry removed. Under 1.x every generate replaced the single stored key, which failed any signature still in flight against the old kid. The command also refuses to publish a duplicate kid, since consumers resolve keys by kid. -
dev.ucp.shopping.permalink(new root capability in 2026-08-25). Hands an agent a real buyable storefront URL instead of an API transaction. Unlike every other toggle, this one needs no endpoint module — a stock Magento cart URL already satisfies it — so it is safe to enable on any store.config.endpointdefaults to{baseUrl}/checkout/cart. -
dev.ucp.shopping.buyer_consent(new extension in 2026-08-25). Extends cart and/or checkout; pruned when neither parent is declared, like every other extension. The admin comment is explicit that it should only be enabled once checkout actually records the consent it receives. -
MCP bindings now declare their OpenRPC
schema. 1.4.0 emitted an MCP binding withendpointbut no schema, leaving agents no machine-readable description of the MCP surface; the spec's own business-profile example declares one on every transport that defines it.
-
Ed25519 keys were unpublishable.
Configrequiredkid/kty/crv/x/yon every stored JWK, so an OKP key — which correctly has noy— was discarded with a warning. Validation now followsprofile.json#/$defs/jwk_public_key:kidandktyare always required, EC additionally needscrv/x/y, OKP needscrv/x, and the kty/crv/alg vocabularies are treated as OPEN, so an unrecognised key type is published rather than dropped (the spec requires that an unsupported key affect only the signature referencing it, never the whole profile). -
alg/crvconsistency is now enforced. The new schema pins ES256 to P-256, ES384 to P-384 and EdDSA to Ed25519; a hand-edited JWK with a mismatched pair failed schema validation with no local warning. -
Private-material stripping extended to the
othandkJWK members, which the 2026-08-25 key schema also forbids. 1.x stripped onlyd/p/q/dp/dq/qi. -
specURLs are no longer authority-bound. 1.x rejected anydev.ucp.*entity whosespecURL was not onucp.dev. The new spec is explicit that documentation is off the machine trust path: aspecURL MUST be https but MAY be served from any host. The check now applies toschemaonly, which is where the binding actually belongs.
UCP_SPEC_TAGbumped tov2026-08-25.dev/schema-validation/validate.pyfollows the schema move:source/discovery/profile_schema.json#/$defs/business_profilewas deleted and replaced bysource/schemas/profile.json#/$defs/business_schema. Both layouts are probed so the validator still works if the tag is rolled back.- Two new fixtures: a permalink-only profile (the realistic shape for a store
with no endpoint module) and a multi-parent extension profile exercising
the array form of
extends. The full fixture publishes both an Ed25519 and an ES256 key, mirroring the spec's own example.
composer require angeo/module-ucp:^2.0 && bin/magento setup:upgrade- Run
bin/magento angeo:ucp:validateand fix anything it reports. - If any platform integrates against your current profile, freeze a copy
of the 2026-04-08 profile as a static file and declare it under
Advanced → Supported Versions:
{"2026-04-08": "https://yourstore.com/ucp/profiles/2026-04-08.json"}. This module serves only the version it advertises, so the older profile has to be hosted by you. - Purge
/.well-known/ucpfrom any CDN or Varnish in front of the store. - Existing ES256 keys keep working unchanged and are simply re-published
under
keys[]. Their kid is left alone — regenerating with a thumbprint kid is only necessary if you intend to use Web Bot Auth.
Extensibility release: multi-transport service bindings and automated schema-drift detection. Spec version 2026-04-08 remains the latest UCP release; the official roadmap signals Lodging and Food verticals next, so the services registry is now open for extension without core changes.
- MCP transport binding. New admin field Transport → MCP Endpoint URL
(
angeo_ucp/transport/mcp_endpoint). When set, the profile advertises an additionalmcpbinding fordev.ucp.shoppingalongside REST — perschemas/service.json, each transport binding is a separate entry in the same service array, andmcp(likerest) REQUIRES anendpointin business profiles. The MCP endpoint is opt-in only and never derived from the store base URL: an MCP server (e.g.angeo/module-mcp-server's Streamable HTTP endpoint) is separate infrastructure. Api\ServiceBindingProviderInterface(@api) + DI provider pool.ProfileGeneratorno longer hardcodes the services registry; it merges registry fragments from a di.xml-configured pool (serviceBindingProviders). Built-in providers:Model\Service\RestShoppingBindingProvider(extracted, behavior unchanged) andModel\Service\McpShoppingBindingProvider. Third-party modules — a future catalog implementation, additional transports, or the upcoming Lodging/Food verticals — contribute bindings via their own di.xml without touching this module. Malformed fragments (non-reverse-domain keys, non-list values) are dropped defensively.- CI schema validation (
.github/workflows/schema-validation.yml+dev/schema-validation/). Fixture profiles are generated through the real module code (framework interfaces stubbed, no Magento install needed) and validated against the OFFICIAL JSON Schemas at the pinned spec tag on every push/PR and weekly. When the next dated spec release lands, bumpingUCP_SPEC_TAGimmediately reveals any schema drift. Fixtures cover: full profile (REST + MCP), minimal, MCP-only, and the orphan-extension pruning case.
Config::getRestEndpoint()/getMcpEndpoint()trim surrounding whitespace from configured values, preventing a whitespace-only value from being advertised as an endpoint.dev.ucp.shoppingservice name centralised asConfig::SHOPPING_SERVICE_NAME.
ProfileGenerator::__construct()gained a second argument (array $serviceBindingProviders = []). Wired automatically via di.xml; only code instantiating the class directly (e.g. tests) needs updating.- Behavior is unchanged for existing installs: with no MCP endpoint configured, the emitted profile is byte-identical to 1.3.0.
Schema-compliance release. The generated profile is now validated end-to-end
against the official JSON Schemas from the spec repository
(Universal-Commerce-Protocol/ucp, tag v2026-04-08:
source/discovery/profile_schema.json, source/schemas/ucp.json,
service.json, capability.json, payment_handler.json). Spec version
2026-04-08 remains the latest UCP release as of July 2026.
ucp.servicesanducp.payment_handlerswere omitted when empty — schema violation.ucp.json#/$defs/business_schemamarks BOTH keys as REQUIRED in a business profile, even when empty. Previouslypayment_handlerswas dropped when not configured andserviceswas emitted as an empty value only implicitly. Both keys (pluscapabilities) are now always present.- Empty registries JSON-encoded as
[]instead of{}— type violation.services,capabilities, andpayment_handlersare JSON objects keyed by reverse-domain name. Empty PHP arrays encode as JSON arrays, so a store with no capabilities served a profile that failed schema validation ([] is not of type 'object'). Empty registries are now emitted asstdClassand serialize as{}. - Admin-supplied
payment_handlerswere passed through unvalidated. Perpayment_handler.json#/$defs/base, every handler entry REQUIRES a stringidand aYYYY-MM-DDversion; the registry must be keyed by reverse-domain names with array values.Config::getPaymentHandlers()now enforces all of this, skipping invalid keys/entries with logged warnings so the served profile always validates. A single entry object is tolerated and wrapped into the required array form.
- CORS preflight support. The
/.well-known/ucpaction now also implementsHttpOptionsActionInterfaceand answersOPTIONSwith204 No ContentplusAllow,Access-Control-Allow-*, andAccess-Control-Max-Ageheaders. Previously Magento's HTTP-method validation rejected OPTIONS even though the response advertisedAccess-Control-Allow-Methods: GET, OPTIONS. The preflight responds identically whether or not the profile is enabled, so it does not leak the advertised state. - Controller unit tests covering the OPTIONS preflight, the disabled
404 (
no-store), the 200 response headers required by the spec's hosting rules (Content-Type: application/json, CORS,Cache-Control: publicwithmax-age >= 60), and the 500 error path. - Admin warning on Identity Linking. The
identity_linking_enabledfield now documents that declaring this capability requires publishing OAuth 2.0 authorization server metadata at/.well-known/oauth-authorization-server(RFC 8414), which is outside this module's scope.
angeo:ucp:validaterewritten against the official schemas. New checks: presence of the requiredservices/payment_handlerskeys; reverse-domain pattern for all registry keys;endpointREQUIRED for rest/mcp/a2a service bindings in business profiles; payment handlerid/versionrequirements;kid/ktyREQUIRED on signing keys. Relaxed to match the business schema: capabilityspec/schemaare now warnings (optional at business level; onlyversionis REQUIRED), and an emptyservicesregistry is a warning rather than an error (schema-valid, but agents have nothing to call).- Reverse-domain validation uses the exact pattern from the spec's
reverse_domain_name.jsontype (Config::REVERSE_DOMAIN_PATTERN).
- Protocol version
2026-04-08is still the latest UCP release. signing_keyscorrectly published as a top-level sibling ofucp(profile_schema.json#/$defs/base).- Catalog correctly split into
catalog.search+catalog.lookup; extensions correctly pruned when orphaned;identity_linkingcorrectly underdev.ucp.commonwithconfig.scopesas an object map; ES256 / P-256 JWK signing keys match the Signatures spec (RFC 9421 foundation).
Fixes the endpoint wiring so /.well-known/ucp is actually served by the
controller, and removes a conflicting static-file code path.
- Router registered in the wrong area — endpoint always 404'd. The
RouterListentry for the custom router was placed in the globaletc/di.xml. The frontend "standard"RouterListis frontend-scoped, so a global registration is not reliably applied to it: the router'smatch()never ran and every request fell through to the CMS router (HTTP 404,text/html, with Magento page-cache tags). The registration is now inetc/frontend/di.xml, which actually wires the router into frontend routing. This was the underlying cause behind the earlier 404 reports. - 404 on LiteSpeed / Hostinger (and other non-nginx servers). The router
matched only
getPathInfo()with a strict comparison. On LiteSpeed thePATH_INFOfor a dot-segment path is frequently empty, so the comparison failed and the request fell through to Magento's CMS router (HTTP 404,text/html). The router now checksgetPathInfo(),getOriginalPathInfo()andgetRequestUri(), strips the query string, and normalises slashes, so it matches/.well-known/ucpregardless of how the web server populates the path. - Missing DI preference for
ProfileGeneratorInterface. The controller and theangeo:ucp:validatecommand depend on the interface, but no<preference>bound it toModel\ProfileGenerator, sosetup:upgradefailed with "Cannot instantiate interface Angeo\Ucp\Api\ProfileGeneratorInterface". This was latent until the router was wired up (nothing previously instantiated the controller). The preference is now declared inetc/di.xml. - Custom router was never registered.
Controller\Routerexisted but noRouterListentry referenced it (and there was noetc/frontend/routes.xml), so the controller was dead code. As a result/.well-known/ucpeither returned 404, or — when a static file was present — was served by the web server asapplication/octet-streamwith no CORS headers, failing UCP validation. The router is now registered inetc/di.xmlviaRouterList(sortOrder 22) and aetc/frontend/routes.xmldeclares the route, so the controller serves the endpoint with the correct headers. - Missing CORS headers. The controller now sends
Access-Control-Allow-Origin: *andAccess-Control-Allow-Methods: GET, OPTIONSon every response, as required for cross-origin fetches by AI agents.
- Static-file approach deleted.
Service\WellKnownWriter,Service\UcpProfileBuilder, and theangeo:ucp:generatecommand (Console\Command\GenerateWellKnown) were removed. Writing a staticpub/.well-known/ucpconflicted with the controller — the web server served the static file first, with the wrong Content-Type — and the builder was a stub duplicate ofModel\ProfileGenerator. The controller is now the single source of truth for the endpoint. - Removed a duplicate
di.xmlfrom the module root (Magento only readsetc/di.xml).
- Removed macOS
__MACOSXresource-fork artifacts from the package.
Full review against the live UCP 2026-04-08 specification at ucp.dev. One spec-compliance defect was found and fixed; the profile generator now covers every business-profile feature defined by the spec.
- [SPEC] Catalog capability naming corrected. The spec defines catalog as
TWO granular capabilities —
dev.ucp.shopping.catalog.searchanddev.ucp.shopping.catalog.lookup— with distinct spec pages (/specification/catalog/search,/specification/catalog/lookup) and schemas (catalog_search.json,catalog_lookup.json). The previous monolithicdev.ucp.shopping.catalogcapability does not exist in the spec and would not match any platform's declared capability during the intersection algorithm, silently disabling catalog for all AI agents. Backwards compatible: the legacycatalog_enabledconfig value is still honoured and enables both granular capabilities.
- Extensions with
extendsdeclarations per spec:dev.ucp.shopping.fulfillment(extendsdev.ucp.shopping.checkout)dev.ucp.shopping.discount(extends checkout and/or cart; emits a string for a single parent and an array for multiple parents, per spec)
- Orphaned-extension pruning. Extensions are never advertised when no parent capability is enabled — the spec prunes orphans during negotiation, so advertising one is always dead weight. The generator enforces this and the validator reports it as an error.
- Identity-linking OAuth scopes. New admin field publishes
config.scopes(e.g.dev.ucp.shopping.order:read) underdev.ucp.common.identity_linking; scope values encode as empty JSON objects{}exactly as the spec example shows. supported_versions(spec SHOULD for businesses supporting older protocol versions): admin JSON field, validated — keys must be YYYY-MM-DD, URIs must be HTTPS; invalid entries are skipped with a logged warning.payment_handlersdeclaration: admin JSON field, validated to be a JSON object keyed by reverse-domain handler name; arrays and malformed JSON are rejected with a logged warning. Documentation warns that profile contents are public and must not contain secret keys.- Validator upgraded to a spec-conformance checker:
- capability required fields (
version,spec,schema— spec: REQUIRED) - service-binding required fields (
version,spec,transport) - Spec URL Binding:
dev.ucp.*capabilities must point athttps://ucp.dev/origins (spec: MUST; platforms reject mismatches) - orphaned-extension detection
- trailing-slash warning on endpoints (spec: SHOULD NOT)
supported_versionsformat checks- private-key-material detection in published
signing_keys
- capability required fields (
- README rewritten with an explicit protocol-coverage matrix documenting which UCP layers this module implements (discovery) and which require separate endpoint implementations (negotiation, transactional REST endpoints, RFC 9421 response signatures, payment handler execution).
- Admin capability section renamed to "Declared Root Capabilities" with a new "Declared Extensions" section; comments clarify that enabling a capability advertises it but does not implement the endpoint.
module.xmlsetup_version and composer version bumped to 1.1.0.
This module deliberately implements the discovery layer of UCP (business profile + signing keys + hosting rules). The transactional flow — capability negotiation, catalog/cart/checkout/order endpoints, RFC 9421 message signatures, payment execution — is the responsibility of endpoint modules that this profile advertises. Enable capability toggles only when the matching endpoint implementation is installed.
First production-ready release. All items below are security fixes or
reliability improvements over the 0.1.x beta line.
-
[SECURITY] HTTP endpoint validation in
Config::getRestEndpoint(). Configured and auto-derived endpoint URLs are now checked for thehttps://scheme. Awarningis emitted tosystem.logif a non-HTTPS URL is detected, alerting the operator before AI agents reject the profile. The UCP spec requires HTTPS for/.well-known/ucp. -
[SECURITY] Over-length coordinate rejection in
JwkFormatter. Coordinates longer than 32 bytes (the P-256 fixed width) are now rejected with aRuntimeExceptioninstead of being silently included. Previously only short coordinates were left-padded; an over-long value from a malformed key would have produced an invalid JWK without error. -
[SECURITY] Security response headers added to all controller responses.
X-Content-Type-Options: nosniff,X-Frame-Options: DENY, andReferrer-Policy: no-referrerare now set on every response from the UCP controller (200, 404, and 500). This aligns with standard Magento hardening practices. -
[SECURITY] JWK entry validation before returning public keys.
Config::getPublicSigningKeys()now validates that each entry in the stored JWK array contains the required fields (kid,kty,crv,x,y) before including it in the profile. Malformed entries are skipped and awarningis logged. -
[SECURITY] Empty
kidrejection inJwkFormatter.publicKeyToJwk()now throws if$kidis an empty string, preventing a JWK with a blank key identifier from being emitted.
-
GenerateKeysCommandscope mismatch. The existing-key check now reads atSCOPE_TYPE_DEFAULT(matching the scope used byConfigWriter::saveConfig()). Previously reading at store scope could return an empty string even when a key was already stored at default scope, allowing the command to silently overwrite it. -
GenerateKeysCommandusesinvalidate()instead ofcleanType().TypeListInterface::invalidate('config')marks the config cache as dirty without flushing unrelated cache types.cleanType()is a heavier operation that is unnecessary here. -
ValidateProfileCommandHTTPS check. The validator now explicitly checks that every declared service-binding endpoint URL useshttps://. Previously a profile with anhttp://endpoint would pass validation even though the UCP spec forbids it. -
ValidateProfileCommandseverity adjustment. The "no capabilities and no signing keys" condition is now awarningrather than a hardFAILURE. A discovery-only deployment that has no capabilities yet is structurally valid; operators are warned but the command exits0so CI pipelines are not broken during initial setup. -
KeyGeneratorusesextension_loaded()instead offunction_exists()to check for OpenSSL.function_exists('openssl_pkey_new')is true even when the extension is partially initialised on some platforms;extension_loaded('openssl')is the canonical check. -
KeyGeneratorkid entropy increased from 2 to 4 bytes. 4 bytes of CSPRNG output (8 hex characters, ~4 billion distinct values) makes accidental kid collisions negligible even in automated high-rotation environments. -
module.xmlnow declaressetup_version="1.0.0".
extra.branch-aliasremoved fromcomposer.json; no longer needed for a stable tagged release.
- Removed the static
"version"field fromcomposer.json. Packagist now reads the version from the git tag, which is the canonical Composer practice and eliminates the risk of the manifest version drifting from the released tag.
- Installation now requires the explicit
@betastability qualifier:composer require angeo/module-ucp:^0.1@beta. - README updated: roadmap row, install instructions, and a note that
0.1.0is yanked. extra.branch-aliasmappingdev-mainto0.1.x-devadded.
- Critical:
composer.jsonwas invalid JSON (trailing comma aftersupport.source), preventingcomposer requirefrom succeeding. magento/module-storeis now explicitly required incomposer.jsonand listed inmodule.xml<sequence>.- Removed dead
etc/frontend/routes.xml. - Removed unused imports in
Controller/WellKnown/Ucp.php.
Angeo\Ucp\Model\Confignow takes aPsr\Log\LoggerInterfaceconstructor argument.Controller\WellKnown\Ucpnow catches non-JsonExceptionthrowables.
SECURITY.md,ext-opensslandext-jsoncomposer requirements.- Two new
ConfigTestcases covering logging behaviour.
- Initial public release.