angeo / module-ucp
angeo/module-ucp
Spec-compliant Universal Commerce Protocol (UCP) profile generator for Magento 2. Generates /.well-known/ucp at protocol version 2026-04-08 with ECDSA P-256 signing keys, declared capabilities, and proper cache headers. v0.1.x is profile-only — catalog, cart, checkout endpoints land in later releases.
angeo/module-ucp
Spec-compliant Universal Commerce Protocol (UCP) profile generator for Magento 2.
Generates /.well-known/ucp at protocol version 2026-04-08 with ECDSA P-256 signing keys, declared capabilities, and proper cache headers — the discovery layer AI agents fetch first when interacting with your store.
v0.1.x is the profile-only release line. It exposes a valid UCP profile to AI platforms. The actual REST endpoints for catalog, cart, checkout and order land in later versions. Enabling a capability here adds it to the advertised profile but does NOT implement the matching endpoint — leave capabilities disabled in production until the matching endpoint module is installed. The current pre-release is
0.1.1-beta; install it explicitly per Installation below.
What it does
- Serves a spec-compliant UCP profile at
https://yourstore.com/.well-known/ucp - Declares the
dev.ucp.shoppingREST service binding - Lets you toggle which capabilities to advertise: catalog, cart, checkout, order, identity_linking
- Generates ECDSA P-256 signing keys (JWK + PEM) via CLI
- Returns correct
Cache-Control: public, max-age=300headers - Returns
404 ucp_not_advertisedwhen the module is disabled — never a misleading empty profile
Requirements
| Requirement | Version |
|---|---|
| Magento | 2.4.7+ |
| PHP | 8.2 or 8.3 or 8.4 |
| OpenSSL extension | enabled |
Installation
The current 0.1.x line is a beta — install with the explicit @beta stability qualifier:
composer require angeo/module-ucp:^0.1@beta
bin/magento module:enable Angeo_Ucp
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
The @beta qualifier scopes the stability exception to this package only — your project's other dependencies stay locked to whatever minimum-stability you have configured.
Once the 0.1.x line goes stable (planned after a short field-test window) the installation command will simply be composer require angeo/module-ucp.
Note on
0.1.0: an early0.1.0tag was published before the composer.json
was valid and beforemagento/module-storewas declared as a dependency.
0.1.0is treated as yanked — start from0.1.1-betaor later.
Quick start
1. Generate signing keys
bin/magento angeo:ucp:keys:generate
This generates an ECDSA P-256 keypair, saves the public JWK to config, and prints the private PEM to stdout exactly once. Copy the private PEM into app/etc/env.php:
'ucp' => [
'signing_keys' => [
'angeo-ucp-2026-abcd' => '<PEM contents>',
],
],
2. Enable the profile in admin
Stores → Configuration → Angeo → UCP → General → Advertise UCP Profile: Yes.
3. Verify
curl -s https://yourstore.com/.well-known/ucp | python3 -m json.tool
Expected output:
{
"ucp": {
"version": "2026-04-08",
"services": {
"dev.ucp.shopping": [
{
"version": "2026-04-08",
"spec": "https://ucp.dev/2026-04-08/specification/overview",
"transport": "rest",
"endpoint": "https://yourstore.com/rest/V1/ucp",
"schema": "https://ucp.dev/2026-04-08/services/shopping/rest.openapi.json"
}
]
},
"capabilities": {}
},
"signing_keys": [
{
"kid": "angeo-ucp-2026-abcd",
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"use": "sig",
"alg": "ES256"
}
]
}
4. Validate (optional)
bin/magento angeo:ucp:validate --json
Prints a green pass if the profile is well-formed; non-zero exit on validation failure (useful as a cron healthcheck).
Admin configuration
| Path | Field | Purpose |
|---|---|---|
| General → Advertise UCP Profile | Yes/No | Master switch for /.well-known/ucp |
| Capabilities → Catalog | Yes/No | Advertise dev.ucp.shopping.catalog |
| Capabilities → Cart | Yes/No | Advertise dev.ucp.shopping.cart |
| Capabilities → Checkout | Yes/No | Advertise dev.ucp.shopping.checkout |
| Capabilities → Order | Yes/No | Advertise dev.ucp.shopping.order |
| Capabilities → Identity Linking | Yes/No | Advertise dev.ucp.common.identity_linking |
| Transport → REST Endpoint URL | URL | Override the default {baseUrl}/rest/V1/ucp |
| Keys → Public JWK | textarea | Populated by keys:generate |
All capability and transport settings are scoped to the store view — multi-store deployments can advertise different profiles per storefront.
Multi-store on a single domain:
/.well-known/ucpis a single path on the host, so Magento's StoreResolver picks one store view to render it. If multiple store views share a host, the bare host resolves to one of them (typically the default), and that store's profile is what AI agents see. To advertise different UCP profiles per store, give each store its own hostname.
CLI commands
| Command | Purpose |
|---|---|
bin/magento angeo:ucp:keys:generate [--force] |
Generate a new P-256 keypair; print private PEM once, save public JWK to config |
bin/magento angeo:ucp:validate [--json] |
Validate the generated profile structure; optionally print the JSON |
Architecture
HTTP request: GET /.well-known/ucp
│
▼
Angeo\Ucp\Controller\Router (sortOrder=22, runs before CMS router)
│
▼
Angeo\Ucp\Controller\WellKnown\Ucp (HttpGetActionInterface)
│
▼
Angeo\Ucp\Model\ProfileGenerator ← Angeo\Ucp\Model\Config
│ │
│ └── reads core_config_data
▼
JSON response with:
- Content-Type: application/json
- Cache-Control: public, max-age=300
- X-UCP-Version: 2026-04-08
Security model
- Private keys are never stored in the database. The
keys:generatecommand prints them once to stdout. Operators are responsible for placing them inapp/etc/env.phpor a secrets manager. Config::getPublicSigningKeys()strips private JWK fields (d,p,q,dp,dq,qi) as defence-in-depth before serialization, in case private material ever ends up in config by mistake.- HTTPS-only by design. The UCP spec requires HTTPS for
/.well-known/ucp; this module's audit check refuses a non-HTTPS endpoint URL. - Cache headers comply with the spec (
public,max-age >= 60). The module sendsmax-age=300.
Testing
composer install
vendor/bin/phpunit --testsuite=unit
Three test classes cover the critical paths:
ProfileGeneratorTest— protocol version, namespace correctness, JSON encoding, capability togglingJwkFormatterTest— RFC 7518 base64url coordinate encoding, 32-byte P-256 width, private-field rejectionConfigTest— store-base-url fallback, private-key sanitization
CI runs the matrix PHP 8.2 / 8.3 / 8.4 on every push.
Roadmap
| Version | Scope |
|---|---|
| 0.1.1-beta (current) | Profile generator, signing keys, admin UI, CLI, tests, CI |
| 0.2.0 | dev.ucp.shopping.catalog real endpoint (search + lookup) backed by Magento_Catalog |
| 0.3.0 | UCP-Agent header parsing, platform profile fetching, capability intersection |
| 0.4.0 | RFC 9421 HTTP Message Signatures for outgoing responses |
| 0.5.0 | Cart capability |
| 1.0.0 | Production-ready full UCP shopping vertical |
Roadmap will shift as the UCP specification evolves.
License
MIT License — see LICENSE
Maintained by Ievgenii Gryshkun · [email protected]
Changelog
All notable changes to angeo/module-ucp are documented here.
Format follows Keep a Changelog.
Versioning follows Semantic Versioning.
[0.1.1-beta] - 2026-05-23
Fixed
- 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.
Changed
- Installation now requires the explicit
@betastability qualifier:
composer require angeo/module-ucp:^0.1@beta. This is documented in
the README and scopes the stability exception to this package only. - README updated: roadmap row, install instructions, and a note that
0.1.0is yanked. - composer.json description updated to reference
v0.1.xrather than
a single point release, since this is the line label.
Added
extra.branch-aliasmappingdev-mainto0.1.x-devfor users who
want to track the development branch via Composer.
Notes
- All v0.1.x releases are profile-only. Enabling a capability adds
it to the advertised profile but does NOT implement the corresponding
REST endpoints. Real catalog/cart/checkout endpoints land in 0.2.0+.
[0.1.0-beta] - 2026-05-23
Fixed
- Critical:
composer.jsonwas invalid JSON (trailing comma after
support.source), preventingcomposer requirefrom succeeding. magento/module-storeis now explicitly required incomposer.json
and listed inmodule.xml<sequence>. Previously the module relied
on transitive loading viaMagento_Backend, which works in practice
but fails the Magento Extension Quality Program (MEQP) checks.- Removed dead
etc/frontend/routes.xml. The frontend route never
resolved/.well-known/ucpanyway (Magento frontNames cannot contain
dots), and the customAngeo\Ucp\Controller\Routerdispatches the
action directly. - Removed unused imports in
Controller/WellKnown/Ucp.php.
Changed
Angeo\Ucp\Model\Confignow takes aPsr\Log\LoggerInterfaceas a
third constructor argument. Throwables fromStoreManager::getStore()
during endpoint resolution and JSON-decode failures on stored JWKs
are now logged aterror/warninginstead of swallowed silently.- When
Config::getPublicSigningKeys()strips private JWK fields
(d,p,q,dp,dq,qi), awarningis now logged so the
operator can see they pasted a private key by mistake. Controller\WellKnown\Ucpnow also catches non-JsonException
throwables from the generator and returns500 profile_generation_failed
instead of bubbling.
Added
SECURITY.md— vulnerability reporting policy and the private-key
custodianship model.ext-opensslandext-jsonnow declared as composer requirements.- Two new ConfigTest cases covering the new logging behaviour.
[0.1.0] - 2026-05-21 (yanked — composer.json was invalid JSON)
Added
- Initial public release. Spec-compliant
/.well-known/ucpprofile
generator at UCP protocol version2026-04-08, custom router,
ECDSA P-256 key generator, profile validator CLI, admin
configuration, PHPUnit suite, GitHub Actions CI.
This tag is treated as yanked because the published composer.json
contained a trailing comma that made it invalid JSON. Composer cannot
install it. Use 0.1.1-beta or later.
| Version | Stability | QA Status | Released |
|---|---|---|---|
| 0.1.1-beta | beta | Incomplete | 2026-05-23 17:53:53 |
| 0.1.0-beta | beta | Not tested | 2026-05-23 17:28:26 |
Requires 7
| Package | Constraint |
|---|---|
| ext-json | * |
| ext-openssl | * |
| magento/framework | >=103.0.0 |
| magento/module-backend | >=102.0.0 |
| magento/module-config | >=101.0.0 |
| magento/module-store | >=101.0.0 |
| php | >=8.2 |
Requires-dev 3
| Package | Constraint |
|---|---|
| magento/magento-coding-standard | * |
| phpstan/phpstan | ^1.10 |
| phpunit/phpunit | ^10.0 |
| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Pending | 0 | |
| PHPStan | Pending | 0 | |
| Cpd | Pending | 0 | |
| Security | Pending | 0 |
Turn an existing module into recurring revenue.
If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.