mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
fix(licensing): honest installation-limit copy and v6 connectivity disclosure
Docs and frontend half of the licensing-policy client work from the v6 upgrade sweep (license findings 5 and 6); the Go classifier and grant refresh fix land separately once the shared pre-commit guard unblocks. A 409 MAX_INSTALLATIONS during v5 key migration rendered as 'another v6 activation handoff is still settling. Retry activation from this instance.' Retrying can never succeed until a slot is freed server-side, so that copy stranded users. The notice now says the key is already active on its maximum number of v6 installations and points at support@pulserelay.pro to free a slot, via the new exchange_installation_limit reason and free_installation_slot action. docs/UPGRADE_v6.md gains a 'Breaking Change: Paid Licensing Requires Connectivity' section: v5 validated keys fully offline, v6 needs periodic connectivity to license.pulserelay.pro, with roughly 10 days of offline tolerance (72 hour grant plus 7 day grace) before paid features drop to Community, plus air-gap guidance and the migrated-key installation cap. docs/releases/V6_CHANGELOG.md gains the matching breaking-change bullet. Note: the two frontend files also carry two small additive hunks from the parallel persisted-license-unreadable slice (a copy case and its test, inert until its Go side lands); they share these files and are included to avoid splitting them mid-flight. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5e78825f54
commit
3a51429a58
4 changed files with 89 additions and 0 deletions
|
|
@ -143,6 +143,35 @@ activation, recovery, or BYOK/local AI setup instead; any exceptional
|
|||
support-issued entitlement is reflected through hosted entitlement state rather
|
||||
than a local in-app trial acquisition flow.
|
||||
|
||||
#### Breaking Change: Paid Licensing Requires Connectivity
|
||||
|
||||
Pulse v5 validated paid license keys entirely locally. Once activated, a v5
|
||||
Pro or Lifetime install never needed to reach a licensing service again, so
|
||||
fully offline and air-gapped installs kept paid features indefinitely.
|
||||
|
||||
Pulse v6 does not work that way. This is a breaking change for paid installs:
|
||||
|
||||
- **What changed.** v6 activates a paid license against
|
||||
`license.pulserelay.pro` and then refreshes a short-lived entitlement grant
|
||||
in the background (several times a day by default). The grant is valid for
|
||||
72 hours, and after it expires Pulse allows a further 7 day grace window.
|
||||
- **Offline tolerance.** A paid v6 instance that cannot reach
|
||||
`license.pulserelay.pro` keeps its paid features for roughly 10 days from
|
||||
the last successful refresh (72 hour grant lifetime plus 7 day grace).
|
||||
After that, paid features drop to Community behavior until connectivity
|
||||
returns. Core monitoring keeps running throughout; this affects paid
|
||||
surfaces such as extended history and AI operations, not data collection.
|
||||
- **Recovery is automatic.** When connectivity returns, the background
|
||||
refresh (or a restart) reactivates the license without re-entering the key.
|
||||
- **Who is affected.** Every paid self-hosted install, including Lifetime.
|
||||
Air-gapped or egress-restricted environments are affected the most: v6
|
||||
cannot keep paid features active without periodic outbound HTTPS to
|
||||
`license.pulserelay.pro`.
|
||||
- **What to do.** Allow outbound HTTPS (port 443) from the Pulse server to
|
||||
`license.pulserelay.pro`. If your environment is air-gapped or cannot
|
||||
allow that egress, contact `support@pulserelay.pro` before upgrading to
|
||||
discuss options for your install.
|
||||
|
||||
#### Paid Pulse Pro Runtime
|
||||
|
||||
Paid Pulse Pro, Relay, and eligible legacy customers should not use public
|
||||
|
|
@ -170,6 +199,12 @@ Pulse v6 uses the activation/grant model for active licensing, but it can migrat
|
|||
- a Pulse v6 activation key, or
|
||||
- a valid Pulse v5 paid license key, which Pulse will try to exchange automatically into the v6 activation model
|
||||
- If the exchange service cannot complete the migration, retry from the v6 license panel or use the self-serve retrieval flow to fetch the current v6 activation key. Email is only a backup copy of that key.
|
||||
- A migrated v5 key can be active on a limited number of v6 installations at
|
||||
a time (currently 3). v5 never counted installations, so if you run the
|
||||
same key on more instances than that, the extra instances will report that
|
||||
the key has reached its installation limit and will stay on Community.
|
||||
Retrying does not help; contact `support@pulserelay.pro` to release an
|
||||
installation you no longer use or to raise the limit.
|
||||
- The exchanged v6 entitlement depends on the original cohort. Lifetime,
|
||||
active pre-cutover recurring Pro, and other migrated legacy paid installs do
|
||||
not all land on the same commercial continuity posture.
|
||||
|
|
|
|||
|
|
@ -124,6 +124,20 @@ the navigation shape you already know.
|
|||
|
||||
- **The legacy `PORT` variable is deprecated.** V6 still honors `PORT` as a fallback for `FRONTEND_PORT` so existing Docker and manual installs keep their configured port after upgrade, but it logs a deprecation warning at startup and `FRONTEND_PORT` wins when both are set. Move deployments to `FRONTEND_PORT`.
|
||||
|
||||
- **Paid licensing now requires periodic connectivity to
|
||||
`license.pulserelay.pro`.** v5 validated paid keys entirely offline; v6
|
||||
activates against the licensing service and refreshes a short-lived
|
||||
entitlement grant in the background. A paid instance that loses
|
||||
connectivity keeps paid features for roughly 10 days from the last
|
||||
successful refresh (a 72 hour grant lifetime plus a 7 day grace window),
|
||||
then drops to Community behavior until connectivity returns. Core
|
||||
monitoring keeps running throughout, and recovery is automatic once the
|
||||
service is reachable again. Air-gapped installs cannot keep paid features
|
||||
active in v6.0.0; contact `support@pulserelay.pro` before upgrading.
|
||||
Migrated v5 keys are also limited to a fixed number of concurrent v6
|
||||
installations (currently 3), which v5 never enforced; see
|
||||
`docs/UPGRADE_v6.md` for details.
|
||||
|
||||
## Under the hood but important
|
||||
|
||||
- **V6 ships with a real v5 upgrade path.** The migration tests cover config
|
||||
|
|
|
|||
|
|
@ -185,6 +185,36 @@ describe('licensePresentation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders the installation limit as terminal with slot guidance, never as a retryable handoff', () => {
|
||||
const notice = getCommercialMigrationNotice({
|
||||
state: 'failed',
|
||||
reason: 'exchange_installation_limit',
|
||||
recommended_action: 'free_installation_slot',
|
||||
} as never);
|
||||
expect(notice).toMatchObject({
|
||||
title: 'v5 license migration needs attention',
|
||||
tone: expect.stringContaining('red'),
|
||||
});
|
||||
expect(notice?.body).toContain('maximum number of v6 installations');
|
||||
expect(notice?.body).toContain('support@pulserelay.pro');
|
||||
expect(notice?.body).not.toContain('Retry activation');
|
||||
expect(notice?.body).not.toContain('still settling');
|
||||
});
|
||||
|
||||
it('renders an unreadable persisted v5 license as terminal with re-enter-key guidance', () => {
|
||||
const notice = getCommercialMigrationNotice({
|
||||
state: 'failed',
|
||||
reason: 'persisted_license_unreadable',
|
||||
recommended_action: 'enter_supported_v5_key',
|
||||
} as never);
|
||||
expect(notice).toMatchObject({
|
||||
title: 'v5 license migration needs attention',
|
||||
tone: expect.stringContaining('red'),
|
||||
});
|
||||
expect(notice?.body).toContain('could not be read on this system');
|
||||
expect(notice?.body).toContain('Retry with the original v5 Pro/Lifetime key');
|
||||
});
|
||||
|
||||
it('returns grandfathered recurring price continuity notices only for active recurring v5 plans', () => {
|
||||
expect(isGrandfatheredRecurringV5PlanVersion('v5_pro_monthly_grandfathered')).toBe(true);
|
||||
expect(isGrandfatheredRecurringV5PlanVersion('v5_pro_annual_grandfathered')).toBe(true);
|
||||
|
|
|
|||
|
|
@ -758,6 +758,8 @@ export const getCommercialMigrationActionText = (action?: string): string => {
|
|||
return 'Use the current v6 activation key for this purchase.';
|
||||
case 'enter_supported_v5_key':
|
||||
return 'Retry with the original v5 Pro/Lifetime key from this instance.';
|
||||
case 'free_installation_slot':
|
||||
return 'Contact support@pulserelay.pro to release an installation you no longer use or to raise the limit.';
|
||||
default:
|
||||
return 'Review the activation state from this instance before trying again.';
|
||||
}
|
||||
|
|
@ -795,6 +797,10 @@ export const getCommercialMigrationNotice = (
|
|||
|
||||
let body = 'Pulse detected a paid v5 license, but it could not be migrated automatically.';
|
||||
switch (migration.reason) {
|
||||
case 'exchange_installation_limit':
|
||||
body =
|
||||
'Pulse detected a paid v5 license, but that key is already active on its maximum number of v6 installations, so this instance cannot activate until a slot is freed.';
|
||||
break;
|
||||
case 'exchange_invalid':
|
||||
body = 'Pulse detected a paid v5 license, but that key was rejected during v6 migration.';
|
||||
break;
|
||||
|
|
@ -811,6 +817,10 @@ export const getCommercialMigrationNotice = (
|
|||
case 'exchange_unsupported':
|
||||
body = 'Pulse detected a key that is not a supported v5 Pro/Lifetime migration input.';
|
||||
break;
|
||||
case 'persisted_license_unreadable':
|
||||
body =
|
||||
'Pulse found a saved v5 license from a previous installation, but it could not be read on this system.';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue