From 7a48cd4afd45d15abd12202f6ace7bb810e3dd82 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 30 Apr 2026 11:01:22 +0100 Subject: [PATCH] Align Relay copy with standalone tier --- SECURITY.md | 4 +-- docs/SCREENSHOTS.md | 2 +- .../v6/internal/subsystems/cloud-paid.md | 6 ++++ .../subsystems/frontend-primitives.md | 7 +++-- .../internal/subsystems/security-privacy.md | 4 ++- frontend-modern/public/docs/SECURITY.md | 4 +-- .../Settings/RelaySettingsPanel.tsx | 2 +- .../RelaySettingsPanel.runtime.test.tsx | 4 +-- .../stores/__tests__/systemSettings.test.ts | 15 +++++++++ .../__tests__/quickstartCopyContract.test.ts | 31 ++++++++++++++++++- .../utils/__tests__/relayPresentation.test.ts | 5 ++- .../src/utils/relayPresentation.ts | 4 +-- 12 files changed, 73 insertions(+), 15 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 1d6be546f..3a7252fb4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -554,14 +554,14 @@ curl -s http://localhost:7655/api/monitoring/scheduler/health | jq Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting. -### Relay Security (Pro) +### Relay Security (Relay and Above) The relay protocol provides mobile remote access with end-to-end encryption: - **ECDH key exchange**: Per-channel encryption keys are derived via Elliptic Curve Diffie-Hellman, meaning the relay server never sees plaintext data. - **Per-channel authentication**: Each mobile session authenticates independently. - **Back-pressure**: Data limiters prevent channel flooding. -- **License-gated**: Relay functionality requires a Pro or Cloud license. +- **License-gated**: Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license. - **Configurable**: Enable/disable via **Settings → Relay** (admin only). ### Agent Command Security diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md index 85589ff5e..8b3817bdc 100644 --- a/docs/SCREENSHOTS.md +++ b/docs/SCREENSHOTS.md @@ -32,4 +32,4 @@ ## Mobile Responsive Design ![Mobile View](images/08-mobile.png) -*Fully responsive mobile interface with a dedicated bottom tab bar for touch navigation. Supports mobile remote access via the relay protocol (Pro feature) for secure monitoring on the go. Touch-optimized controls, adaptive layouts, and the command palette ensure full functionality on smartphones and tablets.* +*Fully responsive mobile interface with a dedicated bottom tab bar for touch navigation. Supports mobile remote access through Pulse Relay for secure monitoring on the go. Touch-optimized controls, adaptive layouts, and the command palette ensure full functionality on smartphones and tablets.* diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 54cb132c9..db52b44de 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -276,6 +276,9 @@ runtime gating as separate unlinked claims. into an operator-actionable activation-required state. Customer-facing Relay settings must not render raw `register:` or license-token-provider diagnostics from the relay client as the primary status message. + Relay settings and public commercial docs must also keep Remote Access + positioned as a Relay-and-higher capability, not a Pro-only feature, so the + Relay tier remains a tangible standalone paid product. 19. Add or change cloud plan presentation through `frontend-modern/src/pages/CloudPricing.tsx` That same presentation boundary also owns truthful customer-entry copy for hosted Cloud pricing and signup. Cloud CTA labels, setup steps, and @@ -1697,6 +1700,9 @@ The paid relay settings surface is part of that same ownership model. Changes to `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx` must carry this contract and the dedicated relay frontend proof files instead of remaining unowned consumers of relay licensing state. +Its paywall and activation copy must say Relay and higher plans rather than +Pro-only wording; Pro may include Relay, but Relay is still its own public +self-hosted tier. That relay settings owner is intentionally split by role: `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx` is the settings shell, `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c8904e946..4a328af50 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -855,7 +855,9 @@ work extends shared components instead of creating new local variants. `frontend-modern/src/utils/relayPresentation.ts`. The route metadata in `settingsHeaderMeta.ts` and the leading `SettingsPanel` in `RelaySettingsPanel.tsx` must reuse the same description and availability - copy instead of drifting into separate rollout or pairing wording. + copy instead of drifting into separate rollout or pairing wording. Relay + availability copy must describe the Relay tier boundary as Relay and higher + plans rather than collapsing Remote Access back into a Pro-only feature. 25. Keep shared settings-shell legal and docs referrals on `frontend-modern/src/utils/docsLinks.ts`. Shared settings surfaces such as `AIRuntimeControlsSection.tsx` must not hardcode GitHub `main` doc URLs for @@ -2640,7 +2642,8 @@ The same shell boundary now also owns shared relay route framing copy: the top-level relay settings description and availability copy used by both `settingsHeaderMeta.ts` and `RelaySettingsPanel.tsx`, so the route shell and its first `SettingsPanel` cannot drift into separate rollout or pairing -descriptions. +descriptions or describe Relay as a Pro-only feature after Relay became its +own self-hosted paid tier. Single-surface settings pages that only render one canonical `SettingsPanel` must stay rooted directly at that panel instead of wrapping it in an extra diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index a6663facc..bbddd80c8 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -292,7 +292,9 @@ silently inheriting whatever older protocol floor the host runtime would allow. That same rule also applies inside shipped security guidance itself: `SECURITY.md` and the synced `frontend-modern/public/docs/SECURITY.md` copy may not bounce the operator back to GitHub `main` for section references that the -running build already owns locally. +running build already owns locally. Their Relay security section must also use +the current Relay-and-higher entitlement boundary instead of stale Pro-only +license wording. That same governed settings trust boundary now also includes `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`, `frontend-modern/src/components/Settings/QuickSecuritySetup.tsx`, diff --git a/frontend-modern/public/docs/SECURITY.md b/frontend-modern/public/docs/SECURITY.md index 1d6be546f..3a7252fb4 100644 --- a/frontend-modern/public/docs/SECURITY.md +++ b/frontend-modern/public/docs/SECURITY.md @@ -554,14 +554,14 @@ curl -s http://localhost:7655/api/monitoring/scheduler/health | jq Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting. -### Relay Security (Pro) +### Relay Security (Relay and Above) The relay protocol provides mobile remote access with end-to-end encryption: - **ECDH key exchange**: Per-channel encryption keys are derived via Elliptic Curve Diffie-Hellman, meaning the relay server never sees plaintext data. - **Per-channel authentication**: Each mobile session authenticates independently. - **Back-pressure**: Data limiters prevent channel flooding. -- **License-gated**: Relay functionality requires a Pro or Cloud license. +- **License-gated**: Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license. - **Configurable**: Enable/disable via **Settings → Relay** (admin only). ### Agent Command Security diff --git a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx index 1215b04a9..b2d755165 100644 --- a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx @@ -30,7 +30,7 @@ import { export const RelaySettingsPanel: Component = (props) => { const state = useRelaySettingsPanelState(props); - // Pro feature gate + // Relay feature gate if (!state.relayEnabled()) { return ( diff --git a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx index d3e83ddbf..befb43bf0 100644 --- a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx @@ -171,7 +171,7 @@ describe('RelaySettingsPanel runtime', () => { expect( screen.getByText( - 'Remote access is available with Relay or Pro. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.', + 'Remote access is available with Relay and higher plans. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.', ), ).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'View plans' })).toHaveAttribute( @@ -257,7 +257,7 @@ describe('RelaySettingsPanel runtime', () => { expect( screen.getByText( - 'Remote Access is enabled, but this instance does not have an active Relay token. Activate Relay or turn Remote Access off before pairing mobile clients.', + 'Remote Access is enabled, but this instance does not have an active Relay token. Activate a Relay-capable plan or turn Remote Access off before pairing mobile clients.', ), ).toBeInTheDocument(); expect(screen.queryByText('register: no license token available')).not.toBeInTheDocument(); diff --git a/frontend-modern/src/stores/__tests__/systemSettings.test.ts b/frontend-modern/src/stores/__tests__/systemSettings.test.ts index 4f5f78268..7711fb91c 100644 --- a/frontend-modern/src/stores/__tests__/systemSettings.test.ts +++ b/frontend-modern/src/stores/__tests__/systemSettings.test.ts @@ -88,6 +88,21 @@ describe('systemSettings store', () => { expect(configurationDoc).toContain('PULSE_TELEMETRY'); }); + it('keeps Relay security guidance aligned with the Relay tier boundary', () => { + const securityDoc = readFileSync(path.join(repoRoot, 'SECURITY.md'), 'utf8'); + const publicSecurityDoc = readFileSync( + path.join(frontendRoot, 'public', 'docs', 'SECURITY.md'), + 'utf8', + ); + + for (const copy of [securityDoc, publicSecurityDoc]) { + expect(copy).toContain('Relay Security (Relay and Above)'); + expect(copy).toContain('Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license'); + expect(copy).not.toContain('Relay Security (Pro)'); + expect(copy).not.toContain('Relay functionality requires a Pro or Cloud license'); + } + }); + it('documents self-hosted AI provider transport and resource-policy redaction in the privacy doc', () => { const privacyDoc = readFileSync(path.join(repoRoot, 'docs', 'PRIVACY.md'), 'utf8'); diff --git a/frontend-modern/src/utils/__tests__/quickstartCopyContract.test.ts b/frontend-modern/src/utils/__tests__/quickstartCopyContract.test.ts index e91a59321..c314a446f 100644 --- a/frontend-modern/src/utils/__tests__/quickstartCopyContract.test.ts +++ b/frontend-modern/src/utils/__tests__/quickstartCopyContract.test.ts @@ -12,13 +12,25 @@ describe('quickstart copy contract', () => { const pulsePro = readRepoFile('docs/PULSE_PRO.md'); const ai = readRepoFile('docs/AI.md'); const privacy = readRepoFile('docs/PRIVACY.md'); + const security = readRepoFile('SECURITY.md'); const publicPrivacy = readRepoFile('frontend-modern/public/docs/PRIVACY.md'); + const publicSecurity = readRepoFile('frontend-modern/public/docs/SECURITY.md'); const pricingSpec = readRepoFile('docs/architecture/v6-pricing-and-tiering.md'); const aiSettingsDialog = readRepoFile( 'frontend-modern/src/components/Settings/AISettingsDialogs.tsx', ); - for (const copy of [readme, pulsePro, ai, privacy, publicPrivacy, pricingSpec, aiSettingsDialog]) { + for (const copy of [ + readme, + pulsePro, + ai, + privacy, + security, + publicPrivacy, + publicSecurity, + pricingSpec, + aiSettingsDialog, + ]) { expect(copy).not.toMatch(/quickstart/i); expect(copy).not.toContain('quickstart:pulse-hosted'); expect(copy).not.toMatch(/hosted AI/i); @@ -28,4 +40,21 @@ describe('quickstart copy contract', () => { expect(copy).not.toMatch(/Pulse Account[\s\S]{0,120}no API key/i); } }); + + it('keeps Relay public docs aligned with the Relay tier instead of Pro-only copy', () => { + const security = readRepoFile('SECURITY.md'); + const publicSecurity = readRepoFile('frontend-modern/public/docs/SECURITY.md'); + const screenshots = readRepoFile('docs/SCREENSHOTS.md'); + + for (const copy of [security, publicSecurity, screenshots]) { + expect(copy).not.toContain('Relay Security (Pro)'); + expect(copy).not.toContain('Relay functionality requires a Pro or Cloud license'); + expect(copy).not.toContain('relay protocol (Pro feature)'); + } + + expect(security).toContain('Relay Security (Relay and Above)'); + expect(publicSecurity).toContain('Relay Security (Relay and Above)'); + expect(security).toContain('Relay, Pro, legacy Pro+, or Cloud license'); + expect(publicSecurity).toContain('Relay, Pro, legacy Pro+, or Cloud license'); + }); }); diff --git a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts index a0355c1f8..a8e3e78ca 100644 --- a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts @@ -106,12 +106,14 @@ describe('relayPresentation', () => { it('centralizes relay availability copy', () => { expect(RELAY_SETTINGS_DESCRIPTION).toContain('Pulse Mobile pairing'); expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('supported Pulse Mobile clients'); - expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('available with Relay or Pro'); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('available with Relay and higher plans'); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).not.toContain('Relay or Pro'); expect(RELAY_PAIRING_AVAILABILITY_TITLE).toBe('Pair Pulse Mobile through Relay'); expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code or deep link'); expect(RELAY_ENABLE_HELP_TEXT).toContain('Pulse Mobile pairing'); expect(RELAY_ACTIVATION_REQUIRED_LABEL).toBe('Activation required'); expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).toContain('active Relay token'); + expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).toContain('Relay-capable plan'); }); it('does not retain retired Relay price or trial-era onboarding copy', () => { @@ -121,5 +123,6 @@ describe('relayPresentation', () => { expect(relayPresentationSource).not.toContain('$49'); expect(relayPresentationSource).not.toContain('$99'); expect(relayPresentationSource).not.toContain('Start free trial'); + expect(relayPresentationSource).not.toContain('Pro feature gate'); }); }); diff --git a/frontend-modern/src/utils/relayPresentation.ts b/frontend-modern/src/utils/relayPresentation.ts index 9d1c58d96..2a69c9b5e 100644 --- a/frontend-modern/src/utils/relayPresentation.ts +++ b/frontend-modern/src/utils/relayPresentation.ts @@ -31,7 +31,7 @@ export const RELAY_DIAGNOSTICS_TITLE_CLASS = 'text-xs font-semibold text-base-co export const RELAY_SETTINGS_DESCRIPTION = 'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.'; export const RELAY_LICENSE_REQUIRED_MESSAGE = - 'Remote access is available with Relay or Pro. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.'; + 'Remote access is available with Relay and higher plans. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.'; export const RELAY_PAIRING_AVAILABILITY_TITLE = 'Pair Pulse Mobile through Relay'; export const RELAY_PAIRING_AVAILABILITY_MESSAGE = 'Supported Pulse Mobile clients connect to this Pulse instance with a QR code or deep link over end-to-end encrypted relay connectivity.'; @@ -39,7 +39,7 @@ export const RELAY_ENABLE_HELP_TEXT = 'Connect this Pulse instance to the relay server for secure remote access and Pulse Mobile pairing.'; export const RELAY_ACTIVATION_REQUIRED_LABEL = 'Activation required'; export const RELAY_ACTIVATION_REQUIRED_MESSAGE = - 'Remote Access is enabled, but this instance does not have an active Relay token. Activate Relay or turn Remote Access off before pairing mobile clients.'; + 'Remote Access is enabled, but this instance does not have an active Relay token. Activate a Relay-capable plan or turn Remote Access off before pairing mobile clients.'; export function getRelayDiagnosticClass(severity: 'warning' | 'error'): string { return severity === 'error'