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.0-beta is the profile-only release. 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 module is tagged
0.1.0-betato signal pre-stable status under semver.
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
composer require angeo/module-ucp
bin/magento module:enable Angeo_Ucp
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
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.0-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.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/ucp anyway (Magento frontNames cannot contain
dots), and the customAngeo\Ucp\Controller\Routerdispatches the
action directly. The leftover routes.xml was misleading. - Removed unused imports in
Controller/WellKnown/Ucp.php
(HttpResponse,ResponseInterface,ActionInterface).
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_failedinstead of bubbling.- Tagged
0.1.0-betarather than0.1.0to signal pre-stable status
explicitly via semver pre-release tags.
Added
.github/workflows/ci.yml— the matrix PHP 8.2/8.3/8.4 GitHub Actions
workflow that the README badge points to. PHPStan job is included
withcontinue-on-erroruntil the full Magento source tree is
available in CI.SECURITY.md— vulnerability reporting policy and the private-key
custodianship model promoted out of README prose into a discoverable
file.ext-opensslandext-jsonnow declared as composer requirements.- Two new ConfigTest cases covering the new logging behaviour
(private-field warning, store-resolution-throw error).
Notes
- v0.1.0-beta is 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 later releases. - Private signing keys are intentionally not stored in the database.
Thekeys:generatecommand prints the private PEM to stdout once;
operators are responsible for placing it inapp/etc/env.phpor a
secrets manager.
[0.1.0] - 2026-05-21 (yanked — composer.json was invalid JSON)
Added
- Spec-compliant
/.well-known/ucpprofile generator at UCP protocol
version2026-04-08. - Custom router (
Angeo\Ucp\Controller\Router) that maps/.well-known/ucp
to a Magento controller without abusingfrontName(dots are not allowed
in Magento frontNames). - ECDSA P-256 signing key generator with JWK output:
bin/magento angeo:ucp:keys:generate. - Profile validator:
bin/magento angeo:ucp:validate [--json]. - Admin configuration under Stores → Configuration → Angeo → UCP with
per-store-view scoping for capability toggles. - Declared capabilities:
dev.ucp.shopping.{catalog,cart,checkout,order}
anddev.ucp.common.identity_linking. - Cache headers per UCP spec:
Cache-Control: public, max-age=300. - PHPUnit test suite (PHP 8.2, 8.3, 8.4).
- GitHub Actions CI matrix.
| 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 |
No QA results yet
QA pipelines haven't run for this version. Status appears here once the vendor publishes a tagged release that gets ingested.
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.