A sync licence, not a shared credential

The registry has always been able to say which packages a registered instance may pull. What it could not say is under what terms, until when, and on whose authority — a PluginGrant was a list of Source/Package entries and nothing else, written in exactly two places: the registration seed, and an admin typing into the Instance-grants tab.

That is fine for three instances. It is not fine for a catalogue, and the pressure showed up the way these things usually do: a consumer that needed one package asked for a standing credential to the whole repository, because there was no smaller thing to ask for.

The grant is now the licence

PluginGrant keeps doing exactly what it did — the registry surface still calls Allows(source, package) and every existing grant keeps working unchanged — but each entry now carries the terms it was issued under:

SyncLicenseService is now the one writer. Issuing is idempotent, revocation is a flag rather than a deletion, and an issuance with no issuing principal is refused rather than written anonymously — a right nobody is recorded as having granted cannot be reviewed with confidence.

And licences that ask something are now enforced

LicenseContent.RequiresAcceptance and the per-user LicenseAcceptance record have existed as node types for a while, with a body hash so an acceptance is evidence against the text that was actually shown. Nothing read them. They are now enforced on the install path, beside the entitlement check and for the same reason — on the action, so the unattended paths are gated identically to a click.

The body hash is checked, not merely stored: an acceptance recorded against earlier terms does not satisfy revised ones. Normalization ignores what a round-trip through git or an editor changes and nothing else, so consent is not revoked by an invisible line ending.

Permissive licences ask nothing and gate nothing — Apache-2.0 and MIT install exactly as before.

Exchange the durable key for a token that expires

POST /api/instances/token trades an instance's durable mwi_ key for a short-lived mwa_ token, narrowed to the packages it actually needs:

POST /api/instances/token
Authorization: Bearer mwi_…
{ "scope": ["Plugins/Publish"], "lifetimeSeconds": 900 }

→ { "accessToken": "mwa_…", "tokenType": "Bearer", "expiresIn": 900,
    "scope": ["Plugins/Publish"] }

Three properties make it safe to hand out freely:

The token itself is signed rather than stored, so minting writes no node and there is no expiry sweep to maintain — which is only safe because authority is re-checked on use.

The signing key is a mesh node, minted on first use at Admin/SyncTokenSigningKey/current and enc:-protected at rest. Nothing to configure, nothing for an operator to copy between environments, and no human ever sees it. Uniqueness across replicas comes from the node: two racing to mint collide on one fixed path, storage keeps the first, and — because the create response tells both callers they won — each reads the stored key back and signs with that. Rotation keeps the outgoing key verifiable for one token lifetime, so rotating never invalidates work in flight.

The point of all of it: a consumer that needs one package can now hold a credential for one package, for fifteen minutes.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.