feat(license): the paste form is self-hosted only
Server Deploy / deploy (push) Successful in 2m14s

A cloud instance's licence is issued in HQ and written into the control
plane by admin/internal/inject, straight to the database. The customer has
nothing to paste, so /settings/license offered them a form that could only
ever fail — and on an expired cloud instance, failed at the exact moment
they were looking for a way out.

GET /license now reports `deployment`, and the page swaps the paste form
for a short panel saying where the licence comes from, with a link to the
portal when HQ_URL is set. That is the same treatment hq-managed members
already get in the members table: read-only here, and a pointer to where
it is actually managed.

POST /license refuses with 409 cloud_managed on a cloud deployment. Hiding
a form is a courtesy; this codebase's rule is that the API is the boundary,
and the endpoint was reachable regardless of what the page rendered. It
cannot break injection, which never goes through HTTP at all.

Verified: server builds and vets clean, web builds clean.
This commit is contained in:
mrhid6
2026-07-26 20:42:53 +01:00
parent fa75d70475
commit dd306e4757
4 changed files with 50 additions and 0 deletions
+25
View File
@@ -206,6 +206,12 @@ export default function LicensePage() {
);
}
// A cloud instance's licence is injected by admin, so there is nothing for
// a customer to paste. The API refuses the POST either way; this is what
// stops the page offering an action that cannot succeed.
const isCloud = license.deployment === "cloud";
const hqUrl = process.env.NEXT_PUBLIC_HQ_URL ?? "";
return (
<div className="p-8">
<div className="mx-auto max-w-5xl space-y-10">
@@ -231,6 +237,24 @@ export default function LicensePage() {
</Card>
</Group>
{isCloud ? (
<Group label="Where this licence comes from">
<Card>
<p className="max-w-prose text-sm text-text-secondary">
This is a cloud instance, so its licence is issued and renewed in Vantage HQ and applied here automatically. There is nothing to paste.
</p>
{hqUrl && (
<div className="mt-5 border-t border-border-soft pt-5">
<a href={hqUrl} target="_blank" rel="noreferrer">
<Button type="button" variant="secondary">
Open Vantage HQ
</Button>
</a>
</div>
)}
</Card>
</Group>
) : (
<Group label="Add or replace">
<Card>
<label htmlFor="licence-blob" className="mb-1.5 block text-sm font-medium text-text-secondary">
@@ -276,6 +300,7 @@ export default function LicensePage() {
</div>
</Card>
</Group>
)}
</div>
</div>
);