docs: implementation plan for spec 7, metered licensing
Ten tasks, each ending in something independently verifiable. The plan stops at the Paddle boundary on purpose: spec 7 lands before plan 5, so there is no client, no webhook and no checkout here, and the configurator is built as a component mounted for staff first rather than as a customer screen with nothing behind it. Corrects two things the spec got wrong about the control plane. Feature gating is already built and mounted, so Free tenants have already lost the console and no customer email is owed. The only real gap is that HandleOIDCCallback lacks the check HandleOIDCStart already has, which is the half that completes a sign-in. Adds the Free self-hosted lifecycle the spec called for and the first draft of the plan missed: linking issues nothing today, and renewInstance hardcodes a monthly term that would hand a self-hosted install a one-month licence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -295,28 +295,28 @@ price, and issues with `ReasonTierChange` as it already would.
|
||||
|
||||
## Control-plane enforcement
|
||||
|
||||
Two of the six columns in the pricing table are enforced by nothing today. A
|
||||
feature picker that sells an ungated checkbox sells nothing.
|
||||
**Feature gating already exists and is already mounted.** `RequireFeature` in
|
||||
`server/internal/api/licence.go` answers 403 `feature_unavailable`, and
|
||||
`server/internal/api/handlers.go` already wraps `POST /api/console/connect`,
|
||||
`GET /api/console/tunnel` and `GET`/`PUT /api/org/oidc` in it. Free's feature list
|
||||
is empty, so a Free instance already cannot open the console. **No capability is
|
||||
taken away from an existing tenant by this spec, and no customer email is owed.**
|
||||
|
||||
**`license.HasFeature` is currently called from nowhere.** Add gates:
|
||||
**One gap remains, and it is a single check.** `HandleOIDCStart` already tests
|
||||
`Feature("oidc")` and redirects to `/login?error=oidc_unavailable`.
|
||||
`HandleOIDCCallback` does not test it at all. A start that 403s is a dead end; an
|
||||
ungated callback completes a sign-in, so the unguarded half is the half that
|
||||
matters.
|
||||
|
||||
- `POST /api/console/connect` and `GET /api/console/tunnel` require
|
||||
`FeatureConsole`.
|
||||
- `GET`/`PUT /api/org/oidc`, `/auth/oidc/start` and `/auth/oidc/callback` require
|
||||
`FeatureOIDC`. The callback matters most: an expired or downgraded licence must
|
||||
not leave a working side door into the instance.
|
||||
The callback cannot copy the start's instance resolution: the start reads
|
||||
`InstanceFromHost(c)`, while the callback resolves the instance from the OAuth
|
||||
state it consumes, and by then it holds `instanceID` directly. The check goes
|
||||
after `ConsumeStateInstance` and before `providerForInstance`, so a licence that
|
||||
lapsed mid-flow stops the exchange rather than completing it.
|
||||
|
||||
A new `FeatureError` maps to 403 with a machine-readable body, mirroring the
|
||||
existing `LimitError`. `web/` hides the Console button and the SSO card when the
|
||||
feature is absent, but as everywhere else in this codebase the API is the
|
||||
boundary and the UI is the courtesy.
|
||||
|
||||
**This removes a capability from existing Free cloud tenants.** Free's features
|
||||
list has always been empty, but nothing gated on it, so a Free instance can use
|
||||
the browser console today and will not be able to afterwards. That is the
|
||||
intended pricing, and it is a deliberate behaviour change rather than a
|
||||
side-effect — it needs to be named in the release note and, ideally, emailed to
|
||||
affected accounts before the gate lands.
|
||||
`web/` hides the Console button and the SSO card when the feature is absent, but
|
||||
as everywhere else in this codebase the API is the boundary and the UI is the
|
||||
courtesy.
|
||||
|
||||
**`CheckMonitorLimit`** joins the three existing checks in
|
||||
`server/internal/services/licence_limits.go`, counting `monitors` for the
|
||||
@@ -445,7 +445,9 @@ sharp edge in the whole migration and it is worth a comment at the decode site.
|
||||
- Free self-hosted can be created, renewed from HQ, and lapses to read-only
|
||||
without being reaped.
|
||||
- Unticking Browser console removes it from the next issued licence, and
|
||||
`POST /api/console/connect` answers 403 on an instance whose licence lacks it.
|
||||
`POST /api/console/connect` answers 403 on an instance whose licence lacks it
|
||||
(already true; the new part is that a customer controls the tick).
|
||||
- `/auth/oidc/callback` answers 403 on an instance whose licence lacks `oidc`.
|
||||
- A monitor beyond the cap is refused with a machine-readable 403.
|
||||
- `audit_logs` older than the licence's retention are gone, and an unlimited
|
||||
licence's are not.
|
||||
|
||||
Reference in New Issue
Block a user