mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 11:37:17 +00:00
fix(keychain): version startup consent policy
This commit is contained in:
+6
-5
@@ -23,7 +23,7 @@ import { useToast } from "./contexts/ToastContext";
|
|||||||
import { setLogStreamActive } from './utils/logger';
|
import { setLogStreamActive } from './utils/logger';
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
import { getPlatformInfo, usePlatformInfo } from './utils/platform';
|
import { getPlatformInfo, usePlatformInfo } from './utils/platform';
|
||||||
import { getKeychainStartupDecision } from './utils/keychainStartup';
|
import { getKeychainConsentVersion, getKeychainStartupDecision } from './utils/keychainStartup';
|
||||||
import { getVersion } from '@tauri-apps/api/app';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import type { PostQueueAction } from './bindings/PostQueueAction';
|
import type { PostQueueAction } from './bindings/PostQueueAction';
|
||||||
import { PanelLeft } from 'lucide-react';
|
import { PanelLeft } from 'lucide-react';
|
||||||
@@ -97,7 +97,7 @@ function App() {
|
|||||||
const platform = usePlatformInfo();
|
const platform = usePlatformInfo();
|
||||||
const [filter, setFilter] = useState<SidebarFilter>('all');
|
const [filter, setFilter] = useState<SidebarFilter>('all');
|
||||||
const [coreReady, setCoreReady] = useState(false);
|
const [coreReady, setCoreReady] = useState(false);
|
||||||
const [appVersion, setAppVersion] = useState('');
|
const [keychainConsentVersion, setKeychainConsentVersion] = useState('');
|
||||||
|
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||||
const stored = Number(window.localStorage.getItem('firelink-sidebar-width'));
|
const stored = Number(window.localStorage.getItem('firelink-sidebar-width'));
|
||||||
@@ -338,13 +338,14 @@ function App() {
|
|||||||
getPlatformInfo().catch(() => null)
|
getPlatformInfo().catch(() => null)
|
||||||
]);
|
]);
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setAppVersion(currentAppVersion);
|
const currentKeychainConsentVersion = getKeychainConsentVersion(currentAppVersion);
|
||||||
|
setKeychainConsentVersion(currentKeychainConsentVersion);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = useSettingsStore.getState();
|
const settings = useSettingsStore.getState();
|
||||||
const { deferKeychainHydration, showKeychainPrompt } = getKeychainStartupDecision({
|
const { deferKeychainHydration, showKeychainPrompt } = getKeychainStartupDecision({
|
||||||
portable: currentPlatform?.portable === true,
|
portable: currentPlatform?.portable === true,
|
||||||
appVersion: currentAppVersion,
|
appVersion: currentKeychainConsentVersion,
|
||||||
approvedVersion: settings.keychainAccessVersion,
|
approvedVersion: settings.keychainAccessVersion,
|
||||||
accessGranted: settings.keychainAccessGranted,
|
accessGranted: settings.keychainAccessGranted,
|
||||||
promptDismissed: settings.keychainPromptDismissed
|
promptDismissed: settings.keychainPromptDismissed
|
||||||
@@ -876,7 +877,7 @@ function App() {
|
|||||||
<AddDownloadsModal />
|
<AddDownloadsModal />
|
||||||
<PropertiesModal />
|
<PropertiesModal />
|
||||||
<DeleteConfirmationModal />
|
<DeleteConfirmationModal />
|
||||||
<KeychainPermissionModal appVersion={appVersion} />
|
<KeychainPermissionModal consentVersion={keychainConsentVersion} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { useSettingsStore } from '../store/useSettingsStore';
|
|||||||
import { invokeCommand as invoke } from '../ipc';
|
import { invokeCommand as invoke } from '../ipc';
|
||||||
import { KeyRound, ShieldAlert } from 'lucide-react';
|
import { KeyRound, ShieldAlert } from 'lucide-react';
|
||||||
import { usePlatformInfo } from '../utils/platform';
|
import { usePlatformInfo } from '../utils/platform';
|
||||||
|
import { getKeychainConsentVersion } from '../utils/keychainStartup';
|
||||||
import { getVersion } from '@tauri-apps/api/app';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
|
|
||||||
type KeychainPermissionModalProps = {
|
type KeychainPermissionModalProps = {
|
||||||
appVersion: string;
|
consentVersion: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = ({ appVersion }) => {
|
export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = ({ consentVersion }) => {
|
||||||
const showKeychainModal = useSettingsStore(state => state.showKeychainModal);
|
const showKeychainModal = useSettingsStore(state => state.showKeychainModal);
|
||||||
const dismissKeychainPrompt = useSettingsStore(state => state.dismissKeychainPrompt);
|
const dismissKeychainPrompt = useSettingsStore(state => state.dismissKeychainPrompt);
|
||||||
const platform = usePlatformInfo();
|
const platform = usePlatformInfo();
|
||||||
@@ -19,11 +20,11 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showKeychainModal || isGranting) return;
|
if (!showKeychainModal || isGranting) return;
|
||||||
const handleEscape = (event: KeyboardEvent) => {
|
const handleEscape = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape') dismissKeychainPrompt(appVersion);
|
if (event.key === 'Escape') dismissKeychainPrompt(consentVersion);
|
||||||
};
|
};
|
||||||
window.addEventListener('keydown', handleEscape);
|
window.addEventListener('keydown', handleEscape);
|
||||||
return () => window.removeEventListener('keydown', handleEscape);
|
return () => window.removeEventListener('keydown', handleEscape);
|
||||||
}, [appVersion, dismissKeychainPrompt, isGranting, showKeychainModal]);
|
}, [consentVersion, dismissKeychainPrompt, isGranting, showKeychainModal]);
|
||||||
|
|
||||||
if (!showKeychainModal) {
|
if (!showKeychainModal) {
|
||||||
return null;
|
return null;
|
||||||
@@ -56,7 +57,7 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
|||||||
try {
|
try {
|
||||||
const result = await invoke('grant_keychain_access');
|
const result = await invoke('grant_keychain_access');
|
||||||
if (result.persistent) {
|
if (result.persistent) {
|
||||||
const grantedVersion = appVersion || await getVersion().catch(() => '');
|
const grantedVersion = consentVersion || getKeychainConsentVersion(await getVersion().catch(() => ''));
|
||||||
// Keep state in sync with the grant result instead of rehydrating
|
// Keep state in sync with the grant result instead of rehydrating
|
||||||
// before Zustand has persisted keychainAccessGranted.
|
// before Zustand has persisted keychainAccessGranted.
|
||||||
useSettingsStore.setState({
|
useSettingsStore.setState({
|
||||||
@@ -79,7 +80,7 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLater = () => {
|
const handleLater = () => {
|
||||||
dismissKeychainPrompt(appVersion);
|
dismissKeychainPrompt(consentVersion);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { getKeychainStartupDecision } from './keychainStartup';
|
import { getKeychainConsentVersion, getKeychainStartupDecision } from './keychainStartup';
|
||||||
|
|
||||||
describe('getKeychainStartupDecision', () => {
|
describe('getKeychainStartupDecision', () => {
|
||||||
|
it('changes the consent identity when the credential-access policy changes', () => {
|
||||||
|
expect(getKeychainConsentVersion('1.1.0')).toBe('1.1.0|keychain-policy-2');
|
||||||
|
expect(getKeychainConsentVersion('')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('re-prompts when the app build keeps the same semantic version', () => {
|
||||||
|
expect(getKeychainStartupDecision({
|
||||||
|
portable: false,
|
||||||
|
appVersion: getKeychainConsentVersion('1.1.0'),
|
||||||
|
approvedVersion: '1.1.0',
|
||||||
|
accessGranted: true,
|
||||||
|
promptDismissed: true
|
||||||
|
})).toEqual({
|
||||||
|
deferKeychainHydration: true,
|
||||||
|
showKeychainPrompt: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('defers persistent hydration and shows the explanation after an update', () => {
|
it('defers persistent hydration and shows the explanation after an update', () => {
|
||||||
expect(getKeychainStartupDecision({
|
expect(getKeychainStartupDecision({
|
||||||
portable: false,
|
portable: false,
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ export type KeychainStartupDecision = {
|
|||||||
showKeychainPrompt: boolean;
|
showKeychainPrompt: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The semantic app version can remain unchanged across release-candidate and
|
||||||
|
// packaging rebuilds. Bump this contract version whenever a build can require
|
||||||
|
// a fresh credential-store authorization, so an updated binary cannot skip
|
||||||
|
// Firelink's explanation and invoke the OS prompt directly.
|
||||||
|
const KEYCHAIN_CONSENT_POLICY_VERSION = '2';
|
||||||
|
|
||||||
|
export const getKeychainConsentVersion = (appVersion: string): string => {
|
||||||
|
const normalizedVersion = appVersion.trim();
|
||||||
|
return normalizedVersion
|
||||||
|
? `${normalizedVersion}|keychain-policy-${KEYCHAIN_CONSENT_POLICY_VERSION}`
|
||||||
|
: '';
|
||||||
|
};
|
||||||
|
|
||||||
export const getKeychainStartupDecision = ({
|
export const getKeychainStartupDecision = ({
|
||||||
portable,
|
portable,
|
||||||
appVersion,
|
appVersion,
|
||||||
|
|||||||
Reference in New Issue
Block a user