license: 180-day post-expiry check-in window; reason-aware 403 (#308) #309

Merged
Cordy merged 7 commits from feat/lic-expiry-sync into main 2026-08-24 01:26:53 +00:00
Owner

Closes #308. Nikola bumped the window from the issue's 60 days to 180 days (6 months).

Before this, two behaviors killed late-renewal self-healing: check-ins stopped the moment the local key expired, and every 403 was treated as revocation (the server 403s for expired too — so a two-day payment hiccup could permanently mark an instance revoked).

  • expiredRetryWindow = 180d: an expired, unrevoked license keeps checking in (existing daily-retry cadence) for six months past expiry; a renewal re-signs the same license id server-side and the next 200 installs it with zero admin action. After the window, an abandoned instance goes quiet for good.
  • checkin() parses the new reason field (license-server#24): only revoked/unknown set st.Revoked. Legacy servers without the field: a bare 403 while the local key is itself expired falls back to the expired outcome.
  • ForceCheckin (#86) gains the same outcome + ErrExpired ("no renewal found yet; the instance keeps checking") instead of a misleading unreachable/revoked error.
  • Status exposes renewalWatchUntil while the watch is live. Enforcement is untouched — an expired license never counts toward the cap; the watch only enables self-healing.

Tests: expired→renew→auto-resync end to end; 403-expired ≠ revoked; 403-revoked still stops; legacy bare-403 fallback; window end goes quiet (both loop and Status).

Follow-up (small, non-blocking): surface renewalWatchUntil in the #admin/licensing panel — the scout shows the panel renders via the licStatusFailed-adjacent status block (~line 3682 web/static/index.html); fits the #302 admin polish pass.

Closes #308. Nikola bumped the window from the issue's 60 days to **180 days (6 months)**. Before this, two behaviors killed late-renewal self-healing: check-ins stopped the moment the local key expired, and every 403 was treated as revocation (the server 403s for *expired* too — so a two-day payment hiccup could permanently mark an instance revoked). - `expiredRetryWindow = 180d`: an expired, unrevoked license keeps checking in (existing daily-retry cadence) for six months past expiry; a renewal re-signs the same license id server-side and the next 200 installs it with zero admin action. After the window, an abandoned instance goes quiet for good. - `checkin()` parses the new `reason` field (license-server#24): only `revoked`/`unknown` set `st.Revoked`. Legacy servers without the field: a bare 403 while the local key is itself expired falls back to the expired outcome. - `ForceCheckin` (#86) gains the same outcome + `ErrExpired` ("no renewal found yet; the instance keeps checking") instead of a misleading unreachable/revoked error. - `Status` exposes `renewalWatchUntil` while the watch is live. Enforcement is untouched — an expired license never counts toward the cap; the watch only enables self-healing. Tests: expired→renew→auto-resync end to end; 403-expired ≠ revoked; 403-revoked still stops; legacy bare-403 fallback; window end goes quiet (both loop and Status). Follow-up (small, non-blocking): surface `renewalWatchUntil` in the `#admin/licensing` panel — the scout shows the panel renders via the `licStatusFailed`-adjacent status block (~line 3682 web/static/index.html); fits the #302 admin polish pass.
Cordy merged commit 0b7f21169b into main 2026-08-24 01:26:53 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#309
No description provided.