PHPackages                             coysh-digital/manager-protocol - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. coysh-digital/manager-protocol

ActiveLibrary

coysh-digital/manager-protocol
==============================

Wire contract shared by the Manager platform and the Manager Connector for Craft CMS.

1.0.1(today)067↑2631.3%1MITPHPPHP &gt;=8.2CI passing

Since Jul 30Pushed todayCompare

[ Source](https://github.com/Coysh-Digital/manager-protocol)[ Packagist](https://packagist.org/packages/coysh-digital/manager-protocol)[ RSS](/packages/coysh-digital-manager-protocol/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (2)Versions (3)Used By (1)

Manager protocol
================

[](#manager-protocol)

The wire contract between the **Manager** platform and the **Manager Connector** for Craft CMS.

This package is deliberately tiny and has **no runtime dependencies** beyond PHP's own `sodium` and `json` extensions. It runs inside privileged plugin code on customer sites, so anything it pulls in would have to be reviewed to the same standard as the connector itself.

It exists as a separate package because the contract is shared by two repositories under different licences: the [control plane](https://github.com/Coysh-Digital/manager) is AGPL-3.0 and the [Craft connector](https://github.com/Coysh-Digital/craft-manager-connector) is MIT. Vendoring copies into both guarantees they drift, and drift in a signing protocol is a security event. Keeping it here means a protocol change is a version bump and two dependent pull requests: deliberate friction, in the one place where surprises are least welcome.

It is MIT, which is the most permissive of the three deliberately. This is the specification of how a site proves its identity to a control plane, and anyone should be able to read, verify or reimplement it without asking us, including to point the connector at something else entirely.

What is in the box
------------------

[](#what-is-in-the-box)

ClassResponsibility`Protocol`Constants: prefixes, header names, tolerances, size caps, the capability list`CanonicalRequest`The exact bytes a connector signs, and path/query canonicalisation`CanonicalResponse`The exact bytes the platform signs, bound to the request nonce`Keys`Ed25519 keypair generation, signing and verification over libsodium`Nonce`Request nonces and single-use enrolment codes, plus their validation and hashing`SchemaValidator`Strict allowlist validation of connector payloads`Sealing`X25519 sealed boxes: handing a key to a recipient without a shared secret`ArtifactStream`Chunked authenticated encryption for backups, so nothing large is held in memory`ArtifactEnvelope`The self-describing header that makes a v2 artifact a file rather than a row`KeyFingerprint`Short, human-comparable names for public keys`RecoveryProof`The challenge and response that prove somebody holds a recovery private key`Jobs`The closed vocabulary of work a platform may ask a site to do`InventorySections`Which report sections each capability permitsRequest signing
---------------

[](#request-signing)

Every connector request carries these headers:

```
Manager-Site:              01JRZX9K2Q4M8N7P6T5V3W2Y1B
Manager-Timestamp:         1785340000
Manager-Nonce:             Zm9vYmFyYmF6cXV4MTIzNA
Manager-Connector-Version: 1.0.0
Manager-Signature:         v1=

```

The signature covers this newline-joined canonical string:

```
MGR1

```

Every field that changes the meaning of a request is in there. Alter the method, the path, the query, the body or the timing and the signature stops verifying — there is a test per field.

The canonical path sorts query parameters, so a proxy that reorders them cannot break verification, and an attacker cannot tamper with them either.

### What the platform must do on receipt

[](#what-the-platform-must-do-on-receipt)

In this order, because each step is cheaper than the next and none of them should be reachable with a payload that failed an earlier one:

1. Reject bodies over `Protocol::MAX_PAYLOAD_BYTES`, before parsing.
2. Require every header in `Protocol::requiredRequestHeaders()`.
3. Reject timestamps outside `Protocol::DEFAULT_TIMESTAMP_TOLERANCE`.
4. Reject a nonce that has been seen. **If the nonce store is unreachable, reject** — this is the one place where availability loses to correctness.
5. Look up the site and verify the signature.
6. Rate-limit by site and by source network.

An unknown site and a bad signature must return an identical response through the same code path. Anything else tells an attacker which site identifiers exist.

TLS is still mandatory. Signing protects integrity and replay; it does nothing for confidentiality.

### Response signing

[](#response-signing)

Responses carrying commands or security-sensitive configuration are signed by the platform over `CanonicalResponse`, which **includes the request nonce**. That binding is the point: without it, a captured response — say, one granting a capability — could be replayed at the connector against a later request.

A connector that expects a signed response and does not get a valid one must treat the request as failed. Never as an unsigned success.

Enrolment codes
---------------

[](#enrolment-codes)

`Nonce::generateEnrolmentCode()` returns 256 bits of CSPRNG output behind an `mgr_enrol_` prefix, so a leaked code is recognisable in a log or a paste.

Only `Nonce::hashEnrolmentCode()` output is ever stored. That is a plain SHA-256, and deliberately not a password hash: the code has no dictionary to slow an attacker down to, and the pairing endpoint has to consume it in a single indexed statement to stay atomic under concurrent requests. Brute force is handled by entropy plus rate limiting, which is where it belongs.

Payload schemas
---------------

[](#payload-schemas)

`schemas/inventory.v1.json` is an allowlist of the operational metadata a connector may report. It maps one-to-one onto the spec's data-minimisation list, and the platform documents why each field is necessary.

`SchemaValidator` **rejects** unknown keys rather than stripping them. This is the single most important behaviour in the package: silently dropping unrecognised fields would let a connector widen what it collects without anyone noticing, whereas failing loudly makes drift visible the moment it appears. Adding a field is a new schema version, not an edit.

Validation errors name the offending path and never quote its value, because those strings reach logs and a rejected payload may be carrying exactly the key material that should not be there.

Two limits of the validator worth knowing before writing a schema against it, because both have already caught somebody out. It implements **no `minLength` and no `minItems`** — `backup.v1.json`carries a `"minLength": 26` that has never enforced anything — so express length as an anchored `pattern` with an explicit quantifier. And PCRE's `$` matches *before* a trailing newline, so `^[0-9a-f]{64}$` accepts `"…\n"`; anchor with `\z`.

Backup artifacts
----------------

[](#backup-artifacts)

A v1 artifact was a bare `ArtifactStream`, with its key sealed to the platform. The platform could read every backup it held, and the connector's documentation said so.

A v2 artifact is an `ArtifactEnvelope` wrapped around the same stream, with the key sealed to an organisation's own recovery keys and to nothing else. The platform stores, verifies, serves and deletes something it cannot open.

```
MGRBAK | major | minor | len | manifest | len | signature | ArtifactStream …

```

The manifest carries everything needed to decrypt the file **offline, with a private key and nothing else** — that is the whole point, because a file that needs our database to describe itself has not achieved zero-knowledge. It is signed by the site's own connector key, so whoever holds the file can confirm which site produced it without asking the platform, which matters because the platform is the party being checked.

The manifest travels as *bytes* everywhere, never as an object to be re-encoded. `backup.v2` carries it base64'd for exactly that reason.

`ArtifactStream` is unchanged between the two formats, deliberately. It has the only genuine cross-implementation byte-compatibility test in this package, and the change from v1 to v2 is about who can obtain the key, not about how bytes are encrypted.

Fixtures
--------

[](#fixtures)

`fixtures/` holds the cross-implementation contract — known inputs with their expected canonical strings and signatures, generated from a fixed seed. Both the platform and the connector test against these files, so a change that breaks byte-compatibility fails in CI rather than in production. See `fixtures/README.md`.

The keypair committed there is test data derived from a fixed seed. It must never be used anywhere real; it is committed precisely so anyone can reproduce the signatures.

Development
-----------

[](#development)

```
composer install
composer test        # pest
composer phpstan     # level 8
```

Security
--------

[](#security)

Report vulnerabilities privately to . Do not open a public issue.

Licence
-------

[](#licence)

**MIT.** See [LICENSE.md](LICENSE.md).

Requires PHP 8.2+ and the `sodium` and `json` extensions. Nothing else.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance100

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5764356?v=4)[Tim Coysh](/maintainers/Coysh)[@Coysh](https://github.com/Coysh)

---

Top Contributors

[![Coysh](https://avatars.githubusercontent.com/u/5764356?v=4)](https://github.com/Coysh "Coysh (13 commits)")

---

Tags

Ed25519protocolmanagercraftcms

###  Code Quality

TestsPest

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/coysh-digital-manager-protocol/health.svg)

```
[![Health](https://phpackages.com/badges/coysh-digital-manager-protocol/health.svg)](https://phpackages.com/packages/coysh-digital-manager-protocol)
```

###  Alternatives

[paragonie/sodium_compat

Pure PHP implementation of libsodium; uses the PHP extension if it exists

932145.2M187](/packages/paragonie-sodium-compat)[socialiteproviders/manager

Easily add new or override built-in providers in Laravel Socialite.

42647.6M589](/packages/socialiteproviders-manager)[graham-campbell/manager

Manager Provides Some Manager Functionality For Laravel

39422.6M144](/packages/graham-campbell-manager)[pestphp/pest-plugin

The Pest plugin manager

4770.7M151](/packages/pestphp-pest-plugin)[clue/redis-protocol

A streaming Redis protocol (RESP) parser and serializer written in pure PHP.

5216.6M14](/packages/clue-redis-protocol)[engram-design/supertable

Super-charge your content builders and create nested Matrix fields.

3131.3k](/packages/engram-design-supertable)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
