fix(keychain): gate credential-store startup access

This commit is contained in:
NimBold
2026-07-15 11:07:48 +03:30
parent 8e02a61c3f
commit 52b00e5cb4
12 changed files with 612 additions and 122 deletions
+95
View File
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest';
import { getKeychainStartupDecision } from './keychainStartup';
describe('getKeychainStartupDecision', () => {
it('defers persistent hydration and shows the explanation after an update', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '1.0.5',
approvedVersion: '1.0.4',
accessGranted: true,
promptDismissed: false
})).toEqual({
deferKeychainHydration: true,
showKeychainPrompt: true
});
});
it('hydrates automatically after the current version was explicitly approved', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '1.0.5',
approvedVersion: '1.0.5',
accessGranted: true,
promptDismissed: false
})).toEqual({
deferKeychainHydration: false,
showKeychainPrompt: false
});
});
it('shows the explanation on first run without touching the credential store', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '1.0.5',
approvedVersion: '',
accessGranted: false,
promptDismissed: false
})).toEqual({
deferKeychainHydration: true,
showKeychainPrompt: true
});
});
it('keeps a deliberately deferred session quiet on later launches', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '1.0.5',
approvedVersion: '1.0.5',
accessGranted: false,
promptDismissed: true
})).toEqual({
deferKeychainHydration: true,
showKeychainPrompt: false
});
});
it('reoffers the explanation after a later update even if the previous one was deferred', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '1.0.6',
approvedVersion: '1.0.5',
accessGranted: false,
promptDismissed: true
})).toEqual({
deferKeychainHydration: true,
showKeychainPrompt: true
});
});
it('does not repeat a deferred prompt when the version API is unavailable', () => {
expect(getKeychainStartupDecision({
portable: false,
appVersion: '',
approvedVersion: '',
accessGranted: false,
promptDismissed: true
})).toEqual({
deferKeychainHydration: true,
showKeychainPrompt: false
});
});
it('never gates portable pairing on credential-store access', () => {
expect(getKeychainStartupDecision({
portable: true,
appVersion: '1.0.5',
approvedVersion: '',
accessGranted: false,
promptDismissed: false
})).toEqual({
deferKeychainHydration: false,
showKeychainPrompt: false
});
});
});
+33
View File
@@ -0,0 +1,33 @@
export type KeychainStartupState = {
portable: boolean;
appVersion: string;
approvedVersion: string;
accessGranted: boolean;
promptDismissed: boolean;
};
export type KeychainStartupDecision = {
deferKeychainHydration: boolean;
showKeychainPrompt: boolean;
};
export const getKeychainStartupDecision = ({
portable,
appVersion,
approvedVersion,
accessGranted,
promptDismissed
}: KeychainStartupState): KeychainStartupDecision => {
if (portable) {
return {
deferKeychainHydration: false,
showKeychainPrompt: false
};
}
const versionChanged = Boolean(appVersion) && approvedVersion !== appVersion;
return {
deferKeychainHydration: !accessGranted || versionChanged,
showKeychainPrompt: versionChanged || (!accessGranted && !promptDismissed)
};
};