# 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.

`composer require angeo/module-ucp`

Canonical URL: https://packagento.com/angeo/module-ucp

## At a glance

- **Vendor**: angeo (https://packagento.com/angeo.md)
- **Latest version**: 1.2.0 — released 2026-06-14
- **Pricing**: Free
- **Package type**: Magento 2 module
- **Status**: active, accepting new buyers

## Installation

Packagento is licence-gated, so even free packages need a licence on a project before Composer can resolve them.

1. **Sign in or create an account** at https://packagento.com/customer/account/.

2. **Add the package to your account.** Open https://packagento.com/angeo/module-ucp and complete the free checkout. A licence is minted automatically.

3. **Create or pick a project, then activate the licence on it.**
   - Projects represent the Magento installs you deploy to. Manage them at https://packagento.com/projects/.
   - Activate the new licence on the project you'll deploy this package to. Activation is what generates the Composer credentials scoped to that project.

4. **Add the project credentials to your Magento codebase.**

   Grab the project's public + private key from https://packagento.com/projects/ (open the project, then its Credentials tab), and add them to `auth.json`:

   ```json
   {
     "http-basic": {
       "packagento.com": {
         "username": "ppk_live_...",
         "password": "psk_live_..."
       }
     }
   }
   ```

   Add the Packagento Composer repository to `composer.json`:

   ```json
   {
     "repositories": [
       { "type": "composer", "url": "https://packagento.com" }
     ]
   }
   ```

5. **Install and apply.**

   ```bash
   composer require angeo/module-ucp:*
   bin/magento setup:upgrade
   bin/magento setup:di:compile
   bin/magento cache:flush
   ```

## What it does

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.

## README

Publishes a [Universal Commerce Protocol](https://ucp.dev) 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

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

Generate signing keys, then verify:

```bash
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](https://keepachangelog.com/en/1.1.0/).
Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### [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](https://ucp.dev/2026-04-08/specification/overview/). One
spec-compliance defect was found and fixed; the profile generator now covers
every business-profile feature defined by the spec.

#### Fixed — spec compliance

_(Changelog truncated for .md surface. Full history on https://packagento.com/angeo/module-ucp.)_

## Recent Versions

| Version | Released |
|---|---|
| 1.2.0 | 2026-06-14 |
| 0.1.1-beta | 2026-05-23 |
| 0.1.0-beta | 2026-05-23 |

## Dependencies

### Require

| 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 |

### Require (dev)

| Package | Constraint |
|---|---|
| magento/magento-coding-standard | * |
| phpstan/phpstan | ^1.10 |
| phpunit/phpunit | ^10.0 |

## Quality

Latest release (1.2.0) fails the Packagento QA pipeline. Verdicts below are per-cell (Magento line × PHP version) for the matrixed tools, and run-once for the static / security tiers.


### Compatibility

Each Magento line is installed on its supported PHP versions, then the module is built (DI compile + static-content deploy). Cells show passed / failed / untested; staircase gaps render as `–`.

| 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. Never affect the Compatibility verdict — 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.

| 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 against a real Magento install. Re-runs per Magento + PHP version because resolvable symbols differ between releases.

| 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 per Magento + PHP cell. Test failures speak to the module's behaviour, not its compatibility with a line, so they're reported here separately.

#### Unit Tests

| 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

| 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

Dependency-advisory audit (composer audit) plus a source malware scan. A malware detection fails the version outright.

| Tool | Status | Findings | Summary |
|---|---|---|---|
| Composer audit | Pass | 0 |  |
| Malware scan | Pass | 0 |  |

## Licence and pricing

Free. A licence is still minted on checkout and bound to your project for Composer access — no payment step.

Refundable within 14 days of first purchase via https://packagento.com/account/refunds/.

## Install via Claude Code or any MCP client

The Packagento MCP server can run the licence + project + Composer steps above in one tool call:

```
purchase_and_install_packages(
  composer_names=["angeo/module-ucp"],
  project_id="proj_xxx"
)
```

This handles cart, checkout, licence minting, project activation, and writes auth.json credentials. Connect a client with `claude mcp add packagento https://mcp.packagento.com`. Full setup at https://packagento.com/docs/mcp-setup.

## Vendor

angeo is a Magento 2 vendor on Packagento. See https://packagento.com/angeo.md for their full catalogue.

