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.

magento2-module 2.4.6-2.4.9 Compatible Based on composer requirements only QA: pending MIT

Packagist Version
License
PHP

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.shopping REST 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=300 headers
  • Returns 404 ucp_not_advertised when 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 early 0.1.0 tag was published before the composer.json
was valid and before magento/module-store was declared as a dependency.
0.1.0 is treated as yanked — start from 0.1.1-beta or 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/ucp is 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:generate command prints them once to stdout. Operators are responsible for placing them in app/etc/env.php or 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 sends max-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 toggling
  • JwkFormatterTest — RFC 7518 base64url coordinate encoding, 32-byte P-256 width, private-field rejection
  • ConfigTest — 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 from composer.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 @beta stability 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.0 is yanked.
  • composer.json description updated to reference v0.1.x rather than
    a single point release, since this is the line label.

Added

  • extra.branch-alias mapping dev-main to 0.1.x-dev for 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.json was invalid JSON (trailing comma after
    support.source), preventing composer require from succeeding.
  • magento/module-store is now explicitly required in composer.json
    and listed in module.xml <sequence>. Previously the module relied
    on transitive loading via Magento_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 custom Angeo\Ucp\Controller\Router dispatches the
    action directly.
  • Removed unused imports in Controller/WellKnown/Ucp.php.

Changed

  • Angeo\Ucp\Model\Config now takes a Psr\Log\LoggerInterface as a
    third constructor argument. Throwables from StoreManager::getStore()
    during endpoint resolution and JSON-decode failures on stored JWKs
    are now logged at error / warning instead of swallowed silently.
  • When Config::getPublicSigningKeys() strips private JWK fields
    (d, p, q, dp, dq, qi), a warning is now logged so the
    operator can see they pasted a private key by mistake.
  • Controller\WellKnown\Ucp now also catches non-JsonException
    throwables from the generator and returns 500 profile_generation_failed
    instead of bubbling.

Added

  • SECURITY.md — vulnerability reporting policy and the private-key
    custodianship model.
  • ext-openssl and ext-json now 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/ucp profile
    generator at UCP protocol version 2026-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.

Versions
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
QA results
Tool Status Findings Summary
PHPCS Pending 0
PHPStan Pending 0
Cpd Pending 0
Security Pending 0
License
MIT
Authors
Make it pay

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.