Show Self-Host Mode section to all users with disabled toggle (#2169)

* Show Self-Host Mode section to all users with disabled toggle for non-lifetime

The Self-Host Mode settings section is now visible to everyone instead of
being hidden behind an eligibility check. The toggle is disabled (greyed out)
for users who are not on a lifetime plan (believer/supporter), making the
feature discoverable while preserving the access restriction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix self-host toggle not disabled after removing license key

Three issues fixed:
1. useIsSelfHostEligible() checked grace period before license key,
   so removing the key didn't revoke eligibility while in grace period.
   Moved license key check to the top and added auto-revocation of
   enableSelfHostMode when key is absent.
2. isSelfHostModeValid() didn't check license key at all, so backend
   code (RetrieverFactory, isPlusEnabled) continued to grant self-host
   access after key removal. Added license key guard.
3. useIsPlusUser() self-host bypass didn't check license key. Added
   the same guard for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix self-host toggle not disabled when switching to non-lifetime key

useIsSelfHostEligible() trusted cached grace period / permanent
validation before consulting the API, so swapping from a believer
key to a plus key kept the toggle enabled.

Now always verifies via API when a license key is present. Cached
validation (grace period, permanent count) is only used as an
offline fallback when the API call fails. Also auto-revokes
enableSelfHostMode when the API confirms the key is not eligible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Logan Yang 2026-02-09 11:34:45 -08:00 committed by GitHub
parent 69f668f7f3
commit 3de16740ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 80 additions and 69 deletions

View file

@ -57,10 +57,13 @@ const SELF_HOST_ELIGIBLE_PLANS = ["believer", "supporter"];
/**
* Check if self-host mode is valid.
* Valid if: permanently validated (3+ successful checks) OR within 15-day grace period.
* Requires license key + toggle enabled + (permanent validation OR within grace period).
*/
export function isSelfHostModeValid(): boolean {
const settings = getSettings();
if (!settings.plusLicenseKey) {
return false;
}
if (!settings.enableSelfHostMode || settings.selfHostModeValidatedAt == null) {
return false;
}
@ -97,8 +100,12 @@ export function isPlusEnabled(): boolean {
*/
export function useIsPlusUser(): boolean | undefined {
const settings = useSettingsValue();
// Self-host mode with valid plan validation bypasses Plus requirements
if (settings.enableSelfHostMode && settings.selfHostModeValidatedAt != null) {
// Self-host mode with valid plan validation bypasses Plus requirements (requires license key)
if (
settings.plusLicenseKey &&
settings.enableSelfHostMode &&
settings.selfHostModeValidatedAt != null
) {
// Permanently valid after 3 successful validations
if (settings.selfHostValidationCount >= SELF_HOST_PERMANENT_VALIDATION_COUNT) {
return true;
@ -146,44 +153,52 @@ export async function isSelfHostEligiblePlan(): Promise<boolean> {
* Hook to check if user should see the self-host mode settings section.
* Returns undefined while loading, boolean once checked.
*
* Eligibility rules (checked in order):
* 1. Permanent validation (count >= 3): Always show (offline-safe, toggle-independent)
* 2. Within 15-day grace period: Always show (offline-safe, toggle-independent)
* 3. Has license key: Check via API (requires online)
* 4. No license key: Hide section
*
* This allows offline re-enable for users who previously validated.
* If grace period expires while offline, user must go online to revalidate.
* Eligibility rules:
* 1. No license key: Not eligible (immediately revokes access)
* 2. Has license key: Verify via API (handles key changes, e.g. believer plus)
* - API success: Use result (revoke self-host mode if not eligible)
* - API failure (offline): Fall back to cached validation
* (permanent count >= 3 OR within 15-day grace period)
*/
export function useIsSelfHostEligible(): boolean | undefined {
const settings = useSettingsValue();
const [isEligible, setIsEligible] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
// Permanently validated users can always see the section (even if toggle is off, offline)
if (settings.selfHostValidationCount >= SELF_HOST_PERMANENT_VALIDATION_COUNT) {
setIsEligible(true);
return;
}
// Users within grace period can see the section (supports offline re-enable)
if (
settings.selfHostModeValidatedAt != null &&
Date.now() - settings.selfHostModeValidatedAt < SELF_HOST_GRACE_PERIOD_MS
) {
setIsEligible(true);
return;
}
// No license key = not eligible, regardless of cached validation state.
// Also force self-host mode OFF so the toggle reflects the revoked state.
if (!settings.plusLicenseKey) {
if (settings.enableSelfHostMode) {
updateSetting("enableSelfHostMode", false);
}
setIsEligible(false);
return;
}
// Check via API for users who haven't enabled self-host mode yet
// Has license key - always verify via API to handle key changes (e.g. believer → plus).
// Fall back to cached validation only when offline.
isSelfHostEligiblePlan()
.then(setIsEligible)
.catch(() => setIsEligible(false));
.then((eligible) => {
if (!eligible && settings.enableSelfHostMode) {
updateSetting("enableSelfHostMode", false);
}
setIsEligible(eligible);
})
.catch(() => {
// Offline fallback: trust cached validation state
if (settings.selfHostValidationCount >= SELF_HOST_PERMANENT_VALIDATION_COUNT) {
setIsEligible(true);
return;
}
if (
settings.selfHostModeValidatedAt != null &&
Date.now() - settings.selfHostModeValidatedAt < SELF_HOST_GRACE_PERIOD_MS
) {
setIsEligible(true);
return;
}
setIsEligible(false);
});
}, [
settings.plusLicenseKey,
settings.enableSelfHostMode,

View file

@ -99,47 +99,43 @@ export const CopilotPlusSettings: React.FC = () => {
}}
/>
{isSelfHostEligible && (
<>
<div className="tw-flex tw-items-center tw-gap-1.5 tw-pt-4 tw-text-xl tw-font-semibold">
Self-Host Mode
<HelpTooltip content="Lifetime license required" />
</div>
<div className="tw-flex tw-items-center tw-gap-1.5 tw-pt-4 tw-text-xl tw-font-semibold">
Self-Host Mode
<HelpTooltip content="Lifetime license required" />
</div>
<SettingItem
type="switch"
title="Enable Self-Host Mode"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">
Use your own infrastructure for LLMs, embeddings (and local document
understanding soon with our upcoming desktop app).
</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
Self-Host Mode (Believer/Supporter only)
</div>
<div className="tw-text-xs tw-text-muted">
Connect to your own self-hosted backend (e.g., Miyo) for complete
control over your AI infrastructure. This allows offline usage and
custom model deployments.
</div>
<div className="tw-text-xs tw-text-muted">
Requires re-validation every 15 days when online.
</div>
</div>
}
/>
</div>
}
checked={settings.enableSelfHostMode}
onCheckedChange={handleSelfHostModeToggle}
disabled={isValidatingSelfHost}
/>
</>
)}
<SettingItem
type="switch"
title="Enable Self-Host Mode"
description={
<div className="tw-flex tw-items-center tw-gap-1.5">
<span className="tw-leading-none">
Use your own infrastructure for LLMs, embeddings (and local document understanding
soon with our upcoming desktop app).
</span>
<HelpTooltip
content={
<div className="tw-flex tw-max-w-96 tw-flex-col tw-gap-2 tw-py-4">
<div className="tw-text-sm tw-font-medium tw-text-accent">
Self-Host Mode (Believer/Supporter only)
</div>
<div className="tw-text-xs tw-text-muted">
Connect to your own self-hosted backend (e.g., Miyo) for complete control
over your AI infrastructure. This allows offline usage and custom model
deployments.
</div>
<div className="tw-text-xs tw-text-muted">
Requires re-validation every 15 days when online.
</div>
</div>
}
/>
</div>
}
checked={settings.enableSelfHostMode}
onCheckedChange={handleSelfHostModeToggle}
disabled={!isSelfHostEligible || isValidatingSelfHost}
/>
</div>
</section>
</div>