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 Compatibility: 2.4.7-2.4.9 Code Quality: Fail Tests: Fail Security: Pass MIT

Are you the maintainer of angeo?

Packagento pulls angeo's Composer packages from the public registry so buyers can find them here.

Claim the namespace to take ownership, publish new releases directly, and start charging for premium versions.

Claim this namespace →

Angeo UCP — /.well-known/ucp for Magento 2

Publishes a Universal Commerce Protocol business profile at
/.well-known/ucp so AI shopping agents (Google/Gemini, ChatGPT, etc.) can
discover your store's commerce capabilities.

  • Profile generated per UCP spec 2026-04-08
  • Served by a PHP controller — correct Content-Type: application/json,
    CORS, and cache headers, with no nginx/Apache changes
  • ECDSA P-256 signing keys, JWK-formatted, public keys only in the profile

How the endpoint is served

/.well-known/ucp is delivered by a controller, not a static file, because the
UCP spec requires Content-Type: application/json and CORS headers
(Access-Control-Allow-Origin: *) — neither of which a static file without an
extension can provide without editing the web server.

Component Role
Controller\Router Custom router matching the exact path /.well-known/ucp and dispatching the action. Registered in etc/di.xml via RouterList (sortOrder 22, before the CMS router). Returns null for any other path.
Controller\WellKnown\Ucp Builds and returns the profile as JSON with Content-Type: application/json, CORS, Cache-Control: public, max-age=300, and hardening headers. Returns 404 when the module is disabled (the site simply does not advertise UCP). Public, no auth — as the spec requires.
Model\ProfileGenerator Builds the spec-2026-04-08 profile (services, capabilities, extensions, payment handlers, supported versions, public signing keys).
Model\Keys\KeyGenerator / JwkFormatter Generates ECDSA P-256 keys and formats the public half as a JWK.

Why this reaches PHP without web-server changes

The stock Magento nginx config ends its main location with
try_files $uri $uri/ /index.php$is_args$args. A request for /.well-known/ucp
with no matching static file falls through to index.php, where the custom
router dispatches it. The official Magento nginx sample has no
location ~ /\. deny rule, so the dot-segment is not blocked.

If your host added a custom location ~ /\. { deny all; } rule it blocks all
dot-paths, and the profile then needs a one-line nginx allow for
^~ /.well-known/. The stock config does not have this problem.

Do not leave a static file at pub/.well-known/ucp: nginx would serve it
first (as application/octet-stream, no CORS) and the controller would never
run.

Install

composer require angeo/module-ucp
bin/magento module:enable Angeo_Ucp
bin/magento setup:upgrade
bin/magento cache:flush

Generate signing keys, then verify:

bin/magento angeo:ucp:keys:generate
curl -sI https://yourstore.com/.well-known/ucp
# HTTP/2 200
# content-type: application/json
# access-control-allow-origin: *
# cache-control: public, max-age=300

CLI

Command Purpose
bin/magento angeo:ucp:keys:generate Generate / rotate the ECDSA P-256 signing key pair.
bin/magento angeo:ucp:validate Validate the generated profile against the UCP spec.

Configuration

Stores → Configuration → Angeo UCP — enable the module and declare which
capabilities your store supports (catalog search/lookup, cart, checkout, order,
identity linking, fulfillment, discount), payment handlers, and supported
protocol versions.

Security

  • The profile is public and unauthenticated by design (per the UCP spec).
    Never put secrets, internal URLs, or admin contacts in it.
  • Only public signing keys are published. The private key never leaves the
    server; ProfileGenerator reads public keys only (getPublicSigningKeys()).
    See SECURITY.md.
  • The endpoint sends X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
    and Referrer-Policy: no-referrer.
  • Rate-limiting is the operator's responsibility (reverse proxy / WAF).
  • If a CDN/Varnish fronts the site, the Cache-Control header lets it cache the
    profile; purge /.well-known/ucp after rotating keys or changing config.

Changelog

All notable changes to angeo/module-ucp are documented here.
Format follows Keep a Changelog.
Versioning follows Semantic Versioning.

[1.2.0] - 2026-06-13

Fixes the endpoint wiring so /.well-known/ucp is actually served by the
controller, and removes a conflicting static-file code path.

Fixed

  • Router registered in the wrong area — endpoint always 404'd. The
    RouterList entry for the custom router was placed in the global
    etc/di.xml. The frontend "standard" RouterList is frontend-scoped, so a
    global registration is not reliably applied to it: the router's match()
    never ran and every request fell through to the CMS router (HTTP 404,
    text/html, with Magento page-cache tags). The registration is now in
    etc/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 the
    PATH_INFO for 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 checks getPathInfo(), getOriginalPathInfo()
    and getRequestUri(), strips the query string, and normalises slashes, so it
    matches /.well-known/ucp regardless of how the web server populates the path.
  • Missing DI preference for ProfileGeneratorInterface. The controller and
    the angeo:ucp:validate command depend on the interface, but no
    <preference> bound it to Model\ProfileGenerator, so setup:upgrade failed
    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 in etc/di.xml.
  • Custom router was never registered. Controller\Router existed but no
    RouterList entry referenced it (and there was no etc/frontend/routes.xml),
    so the controller was dead code. As a result /.well-known/ucp either
    returned 404, or — when a static file was present — was served by the web
    server as application/octet-stream with no CORS headers, failing UCP
    validation. The router is now registered in etc/di.xml via RouterList
    (sortOrder 22) and a etc/frontend/routes.xml declares the route, so the
    controller serves the endpoint with the correct headers.
  • Missing CORS headers. The controller now sends
    Access-Control-Allow-Origin: * and Access-Control-Allow-Methods: GET, OPTIONS
    on every response, as required for cross-origin fetches by AI agents.

Removed

  • Static-file approach deleted. Service\WellKnownWriter,
    Service\UcpProfileBuilder, and the angeo:ucp:generate command
    (Console\Command\GenerateWellKnown) were removed. Writing a static
    pub/.well-known/ucp conflicted with the controller — the web server served
    the static file first, with the wrong Content-Type — and the builder was a
    stub duplicate of Model\ProfileGenerator. The controller is now the single
    source of truth for the endpoint.
  • Removed a duplicate di.xml from the module root (Magento only reads
    etc/di.xml).

Housekeeping

  • Removed macOS __MACOSX resource-fork artifacts from the package.

[1.1.0] - 2026-06-12

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.

Fixed — spec compliance

  • [SPEC] Catalog capability naming corrected. The spec defines catalog as
    TWO granular capabilities — dev.ucp.shopping.catalog.search and
    dev.ucp.shopping.catalog.lookup — with distinct spec pages
    (/specification/catalog/search, /specification/catalog/lookup) and
    schemas (catalog_search.json, catalog_lookup.json). The previous
    monolithic dev.ucp.shopping.catalog capability 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 legacy catalog_enabled config value is still
    honoured and enables both granular capabilities.

Added — full business-profile coverage

  • Extensions with extends declarations per spec:
    • dev.ucp.shopping.fulfillment (extends dev.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) under
    dev.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_handlers declaration: 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 at
      https://ucp.dev/ origins (spec: MUST; platforms reject mismatches)
    • orphaned-extension detection
    • trailing-slash warning on endpoints (spec: SHOULD NOT)
    • supported_versions format checks
    • private-key-material detection in published signing_keys

Changed

  • 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.xml setup_version and composer version bumped to 1.1.0.

Notes — protocol coverage

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.

[1.0.0] - 2026-06-11

First production-ready release. All items below are security fixes or
reliability improvements over the 0.1.x beta line.

Security

  • [SECURITY] HTTP endpoint validation in Config::getRestEndpoint().
    Configured and auto-derived endpoint URLs are now checked for the
    https:// scheme. A warning is emitted to system.log if 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 a RuntimeException instead 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, and
    Referrer-Policy: no-referrer are 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 a warning is logged.

  • [SECURITY] Empty kid rejection in JwkFormatter.
    publicKeyToJwk() now throws if $kid is an empty string, preventing
    a JWK with a blank key identifier from being emitted.

Fixed

  • GenerateKeysCommand scope mismatch.
    The existing-key check now reads at SCOPE_TYPE_DEFAULT (matching the
    scope used by ConfigWriter::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.

  • GenerateKeysCommand uses invalidate() instead of cleanType().
    TypeListInterface::invalidate('config') marks the config cache as
    dirty without flushing unrelated cache types. cleanType() is a
    heavier operation that is unnecessary here.

  • ValidateProfileCommand HTTPS check.
    The validator now explicitly checks that every declared service-binding
    endpoint URL uses https://. Previously a profile with an http://
    endpoint would pass validation even though the UCP spec forbids it.

  • ValidateProfileCommand severity adjustment.
    The "no capabilities and no signing keys" condition is now a warning
    rather than a hard FAILURE. A discovery-only deployment that has
    no capabilities yet is structurally valid; operators are warned but
    the command exits 0 so CI pipelines are not broken during initial
    setup.

  • KeyGenerator uses extension_loaded() instead of function_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.

  • KeyGenerator kid 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.xml now declares setup_version="1.0.0".

Removed

  • extra.branch-alias removed from composer.json; no longer needed
    for a stable tagged release.

[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.
  • README updated: roadmap row, install instructions, and a note that
    0.1.0 is yanked.
  • extra.branch-alias mapping dev-main to 0.1.x-dev added.

[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>.
  • Removed dead etc/frontend/routes.xml.
  • Removed unused imports in Controller/WellKnown/Ucp.php.

Changed

  • Angeo\Ucp\Model\Config now takes a Psr\Log\LoggerInterface constructor argument.
  • Controller\WellKnown\Ucp now catches non-JsonException throwables.

Added

  • SECURITY.md, ext-openssl and ext-json composer requirements.
  • Two new ConfigTest cases covering logging behaviour.

[0.1.0] - 2026-05-21 (yanked — composer.json was invalid JSON)

Added

  • Initial public release.
Versions
Version Stability QA Status Compatibility Released
1.2.0 stable Fail Magento 2.4.7-2.4.9 Details 2026-06-14 18:53:11
0.1.1-beta beta Fail Magento 2.4.7-2.4.9 Details 2026-05-23 17:53:53
0.1.0-beta beta Not tested Not yet tested Details 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

Compatibility

Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.

Compatibility matrix (Magento × PHP)
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Pass Pass
2.4.8 Pass Pass
2.4.9 Pass Pass

Code Quality

Advisory checks against the module's source. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. A phpcs finding can't make a module incompatible.

Static analysis

Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.

Static analysis results
Tool Status Findings Summary
PHPCS Fail 52 3 errors, 49 warnings (ruleset: Magento2)
PHPMD Warning 23 23 rule violations (MissingImport:13, NPathComplexity:3, CyclomaticComplexity:2, ExcessiveMethodLength:2, ExcessiveClassComplexity:1)
Cpd Pass 0
Composer validate Info 5 valid; 5 advisory notes (composer validate --strict)

PHPStan

Type-checks the module's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases.

PHPStan results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 1 1
2.4.8 1 1
2.4.9 1 1

Tests

Unit and integration suites, run for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.

Unit tests

Unit tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 Pass Pass
2.4.8 2 not tested
2.4.9 2 2

Integration tests

Integration tests results by Magento and PHP version
Magento PHP 8.2 PHP 8.3 PHP 8.4 PHP 8.5
2.4.7 N/A N/A
2.4.8 N/A N/A
2.4.9 N/A N/A

Security

Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.

Security results
Tool Status Findings Summary
Composer audit Pass 0
Malware scan Pass 0
License
MIT
Authors

More from angeo

View vendor
angeo/module-llms-txt Free
magento2-module

Magento 2 module for AI Engine Optimization (AEO). Generates spec-compliant llms.txt and llms-full.txt per llmstxt.org standard, plus streaming JSONL for vector indexing. Multi-store, multi-website, CLI, cron, async admin UI, Page Builder-aware sanitization, customer-group pricing, atomic writes, ETag/Cache-Control, .md mirrors.

v3.2.0 18d ago
0
angeo/module-robots-txt-aeo Free
magento2-module

Magento 2 module for AI Engine Optimization (AEO). Injects AI crawler rules (OAI-SearchBot, GPTBot, ChatGPT-User, PerplexityBot, Perplexity-User, Google-Extended, ClaudeBot, anthropic-ai, Claude-User, Applebot, cohere-ai, Amazonbot, Meta-ExternalAgent) into robots.txt — without overwriting your existing configuration. Supports per-bot Allow/Disallow lists, Crawl-delay, Sitemap directives, multi-store, and a public Api\RobotsStatusInterface for cross-module integration with angeo/module-aeo-audit.

v3.0.0 18d ago
0
angeo/module-aeo-brand-visibility Free
magento2-module

Live AI brand visibility audit for Magento 2. Queries ChatGPT, Claude, Perplexity, Gemini and Groq with brand-probing prompts and scores real-world AI recall, citation rate and recommendation presence. Extends angeo/module-aeo-audit v3 via CheckerInterface as the 16th signal, alongside the 15 built-in technical checks.

v1.2.0 18d ago
0
angeo/module-aeo-audit Free
magento2-module

Magento 2 AEO (AI Engine Optimization) Audit. v3 covers 15 signals — robots.txt AI bots, llms.txt + llms.jsonl, Product / Organization / FAQ schema, merchant return + shipping policies, sitemap.xml, UCP profile, AI product feed, OG tags, canonical + hreflang, JSON-LD quality, well-known endpoint matrix, Core Web Vitals via CrUX. Score Trend dashboard, Admin UI, cron, dynamic fix commands, dependency-injected extension point for custom checkers.

v3.1.0 18d ago
0
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.