PHP Composer¶
ArtiGate mirrors PHP Composer packages across a data diode. The low side resolves the require closure from the upstream's Composer v2 (p2) metadata over stable releases and downloads each release's dist zip; each release's expanded version object travels in the signed manifest with its dist and source sections stripped. The high side re-renders the p2 API from those verified objects — re-injecting a dist that points back at its own verified zips — so composer install works against /composer with packagist.org disabled.
Dist URLs always point at the mirror
The upstream dist/source sections are stripped before the metadata crosses the diode (they would leak internal URLs and bypass the mirror), and a manifest that still carries either is rejected at import. The served p2 objects get a fresh dist whose URL is the mirror's own /composer/dist/... path with a recomputed shasum.
How it works¶
package specs ("monolog/monolog", "psr/container:2.0.2")
│
▼
fetch <upstream>/p2/<vendor>/<project>.json ──▶ select release, follow require
│
▼
download each dist zip ──▶ strip dist/source from the metadata
│
▼
signed ArtiGate bundle ══ diode ══▶ high side import
│
▼
re-render packages.json + p2 metadata from the
verified objects, dist re-pointed at /composer/dist/
Low side: input¶
POST /admin/composer/collect (add ?stream=1 for streamed progress, ?dry_run=1 for an estimate). Body limit 1 MiB.
| Field | Type | Meaning |
|---|---|---|
packages |
[]string |
Required. Package specs: vendor/project for the newest stable release, vendor/project:2.0.2 to pin (pretty or normalized version both match) |
no_deps |
bool | Mirror only the listed packages, skipping the require closure |
force |
bool | Bypass the export-dedup index (full, self-contained bundle) |
--composer-repo points the collector at another Composer repository (default https://repo.packagist.org). There is no auth field — private repositories are not supported on this stream. Scheduled watches are supported.
Resolution and download¶
The p2 metadata (<upstream>/p2/<vendor>/<project>.json, Composer's minified format expanded first) drives everything; no composer binary is invoked. A pinned spec picks its exact release; a bare spec picks the newest stable release (falling back to the newest of anything only when no stable exists). The require closure is followed breadth-first, capped at 2000 packages, with three deliberate skips:
- Platform packages —
php,hhvm,composer-plugin-api,composer-runtime-api,ext-*,lib-*— are requirements on the runtime, not mirrorable packages. - Dependencies resolve stable-only (Composer's default
minimum-stability: stable); a require satisfiable only by a beta is reported, not guessed. devconstraints (dev-master,2.x-dev, …) are skipped — the mirror serves tagged releases only.
The constraint language is a pragmatic subset — *, ^, ~ (two-field or more), comparisons, .* wildcards, || alternation, comma/space AND — and anything outside it (hyphen ranges, stability flags, dev-*) is reported per package, never guessed.
Composer's metadata declares no usable digest for dists (the upstream shasum field is empty in practice), so the zip download is TLS-trusted to the metadata-declared URL — http(s) zip dists only — then hash-locked into the signed bundle. Everything after the download is covered by the bundle's SHA-256 chain.
High side: p2 re-rendering¶
On import, each zip is stored (never opened — metadata comes from the verified manifest objects) and its SHA-1 recomputed for the client-facing dist.shasum. Serving regenerates everything on the fly, gated on the zip being present:
| Route | Response |
|---|---|
GET /composer/packages.json |
{"metadata-url": "/composer/p2/%package%.json", "available-packages": [...]} |
GET /composer/p2/<vendor>/<project>.json |
Full version objects, newest first, each with a re-injected mirror dist |
GET /composer/p2/<vendor>/<project>~dev.json |
Always an empty list for known packages (the mirror carries no dev versions) |
GET /composer/dist/<vendor>/<project>/<version_normalized>.zip |
The dist zip |
Client setup¶
// composer.json
{
"repositories": {
"packagist.org": false,
"mirror": { "type": "composer", "url": "https://artigate-high.local/composer" }
},
"require": { "psr/container": "2.0.2" }
}
"packagist.org": false disables the built-in Packagist fallback — required, or Composer will quietly fetch anything the mirror lacks from the internet. Composer re-verifies the served dist.shasum against each downloaded zip.
Limitations¶
- Tagged releases only, stable-first. Dev versions are never mirrored (
~devmetadata is served as an empty list), and dependency edges resolve stable-only. - http(s)
zipdists only — other dist types or schemes fail that package. - No upstream digest: the low-side zip download is TLS-trusted (Composer metadata carries no usable checksum); integrity from there on rests on the signed bundle, and the served shasum is recomputed by the mirror.
- Constraint subset: unsupported constraint forms are reported and skipped, never guessed.
- No private-repository auth on this stream.
- Export dedup applies: an unchanged re-collect is skipped without consuming a sequence number;
"force": truebypasses it. Re-collecting accumulates additional releases of a package.
Related pages¶
- Low side — operating the exporter
- High side — operating the read-only mirror
- Scheduling (watches) — recurring collects
- Security & trust — the signing/verification chain
- HTTP API reference — the exact request/response contracts