mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +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 { openUrl } from '@tauri-apps/plugin-opener';
|
||||
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 type { PostQueueAction } from './bindings/PostQueueAction';
|
||||
import { PanelLeft } from 'lucide-react';
|
||||
@@ -97,7 +97,7 @@ function App() {
|
||||
const platform = usePlatformInfo();
|
||||
const [filter, setFilter] = useState<SidebarFilter>('all');
|
||||
const [coreReady, setCoreReady] = useState(false);
|
||||
const [appVersion, setAppVersion] = useState('');
|
||||
const [keychainConsentVersion, setKeychainConsentVersion] = useState('');
|
||||
|
||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||
const stored = Number(window.localStorage.getItem('firelink-sidebar-width'));
|
||||
@@ -338,13 +338,14 @@ function App() {
|
||||
getPlatformInfo().catch(() => null)
|
||||
]);
|
||||
if (!active) return;
|
||||
setAppVersion(currentAppVersion);
|
||||
const currentKeychainConsentVersion = getKeychainConsentVersion(currentAppVersion);
|
||||
setKeychainConsentVersion(currentKeychainConsentVersion);
|
||||
|
||||
try {
|
||||
const settings = useSettingsStore.getState();
|
||||
const { deferKeychainHydration, showKeychainPrompt } = getKeychainStartupDecision({
|
||||
portable: currentPlatform?.portable === true,
|
||||
appVersion: currentAppVersion,
|
||||
appVersion: currentKeychainConsentVersion,
|
||||
approvedVersion: settings.keychainAccessVersion,
|
||||
accessGranted: settings.keychainAccessGranted,
|
||||
promptDismissed: settings.keychainPromptDismissed
|
||||
@@ -876,7 +877,7 @@ function App() {
|
||||
<AddDownloadsModal />
|
||||
<PropertiesModal />
|
||||
<DeleteConfirmationModal />
|
||||
<KeychainPermissionModal appVersion={appVersion} />
|
||||
<KeychainPermissionModal consentVersion={keychainConsentVersion} />
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,13 +3,14 @@ import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import { KeyRound, ShieldAlert } from 'lucide-react';
|
||||
import { usePlatformInfo } from '../utils/platform';
|
||||
import { getKeychainConsentVersion } from '../utils/keychainStartup';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
|
||||
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 dismissKeychainPrompt = useSettingsStore(state => state.dismissKeychainPrompt);
|
||||
const platform = usePlatformInfo();
|
||||
@@ -19,11 +20,11 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
||||
useEffect(() => {
|
||||
if (!showKeychainModal || isGranting) return;
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') dismissKeychainPrompt(appVersion);
|
||||
if (event.key === 'Escape') dismissKeychainPrompt(consentVersion);
|
||||
};
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}, [appVersion, dismissKeychainPrompt, isGranting, showKeychainModal]);
|
||||
}, [consentVersion, dismissKeychainPrompt, isGranting, showKeychainModal]);
|
||||
|
||||
if (!showKeychainModal) {
|
||||
return null;
|
||||
@@ -56,7 +57,7 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
||||
try {
|
||||
const result = await invoke('grant_keychain_access');
|
||||
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
|
||||
// before Zustand has persisted keychainAccessGranted.
|
||||
useSettingsStore.setState({
|
||||
@@ -79,7 +80,7 @@ export const KeychainPermissionModal: React.FC<KeychainPermissionModalProps> = (
|
||||
};
|
||||
|
||||
const handleLater = () => {
|
||||
dismissKeychainPrompt(appVersion);
|
||||
dismissKeychainPrompt(consentVersion);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getKeychainStartupDecision } from './keychainStartup';
|
||||
import { getKeychainConsentVersion, getKeychainStartupDecision } from './keychainStartup';
|
||||
|
||||
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', () => {
|
||||
expect(getKeychainStartupDecision({
|
||||
portable: false,
|
||||
|
||||
@@ -11,6 +11,19 @@ export type KeychainStartupDecision = {
|
||||
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 = ({
|
||||
portable,
|
||||
appVersion,
|
||||
|
||||
Reference in New Issue
Block a user