fix(ext/sw): inactivity timer resets on all non-passive messages (Plan C Phase 5)
DEV-C P2: an active autofiller never opens the popup, so under the old rule it got force-locked despite continuous use. Inverts the rule: reset on all messages except a documented exclusion set (only get_autofill_candidates today). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import * as timer from '../session-timer';
|
import * as timer from '../session-timer';
|
||||||
|
import { READ_ONLY_CONTENT_CALLABLE } from '../session-timer';
|
||||||
|
|
||||||
describe('session-timer', () => {
|
describe('session-timer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -97,3 +98,29 @@ describe('session-timer', () => {
|
|||||||
expect(cb).not.toHaveBeenCalled();
|
expect(cb).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('READ_ONLY_CONTENT_CALLABLE — inversion exclusion set', () => {
|
||||||
|
// The SW handler invokes resetTimer() on every message whose type is NOT
|
||||||
|
// in this set. These cases encode the documented inversion contract from
|
||||||
|
// Plan C Phase 5: popup-only messages reset, content-callable writes
|
||||||
|
// reset, only passive content reads (currently just get_autofill_candidates)
|
||||||
|
// do NOT reset.
|
||||||
|
|
||||||
|
it('popup-only message would reset the timer (not in exclusion set)', () => {
|
||||||
|
// e.g. list_items — popup interaction is unambiguously active use
|
||||||
|
expect(READ_ONLY_CONTENT_CALLABLE.has('list_items')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('content-callable get_autofill_candidates does NOT reset (in exclusion set)', () => {
|
||||||
|
expect(READ_ONLY_CONTENT_CALLABLE.has('get_autofill_candidates')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('content-callable capture_save_login DOES reset (write op = active use)', () => {
|
||||||
|
expect(READ_ONLY_CONTENT_CALLABLE.has('capture_save_login')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('content-callable check_credential DOES reset', () => {
|
||||||
|
// Asking "is this credential already saved" is user-initiated.
|
||||||
|
expect(READ_ONLY_CONTENT_CALLABLE.has('check_credential')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
/// forwards every message into router/index.route().
|
/// forwards every message into router/index.route().
|
||||||
|
|
||||||
import type { Request, Response, SessionTimeoutConfig } from '../shared/messages';
|
import type { Request, Response, SessionTimeoutConfig } from '../shared/messages';
|
||||||
import { CONTENT_CALLABLE_TYPES } from '../shared/messages';
|
|
||||||
import type { RouterState } from './router/index';
|
import type { RouterState } from './router/index';
|
||||||
import { route } from './router/index';
|
import { route } from './router/index';
|
||||||
import * as vault from './vault';
|
import * as vault from './vault';
|
||||||
import { clearCurrent } from './session';
|
import { clearCurrent } from './session';
|
||||||
import * as sessionTimer from './session-timer';
|
import * as sessionTimer from './session-timer';
|
||||||
|
import { READ_ONLY_CONTENT_CALLABLE } from './session-timer';
|
||||||
|
|
||||||
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
// @ts-ignore TS2307 — resolved by webpack alias / copy
|
||||||
import initDefault, { initSync } from '../../wasm/relicario_wasm.js';
|
import initDefault, { initSync } from '../../wasm/relicario_wasm.js';
|
||||||
@@ -73,7 +73,10 @@ chrome.commands.onCommand.addListener((command) => {
|
|||||||
chrome.runtime.onMessage.addListener(
|
chrome.runtime.onMessage.addListener(
|
||||||
(request: Request, sender: chrome.runtime.MessageSender, sendResponse: (r: Response) => void) => {
|
(request: Request, sender: chrome.runtime.MessageSender, sendResponse: (r: Response) => void) => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (!CONTENT_CALLABLE_TYPES.has(request.type as never)) {
|
// Plan C Phase 5: invert the reset rule. Reset on every message
|
||||||
|
// except a documented passive-read exclusion set, so an active
|
||||||
|
// autofiller / content-driven flow keeps the vault alive.
|
||||||
|
if (!READ_ONLY_CONTENT_CALLABLE.has(request.type)) {
|
||||||
sessionTimer.resetTimer();
|
sessionTimer.resetTimer();
|
||||||
}
|
}
|
||||||
if (!state.wasm) {
|
if (!state.wasm) {
|
||||||
|
|||||||
@@ -48,3 +48,17 @@ export function stopTimer(): void {
|
|||||||
timerId = null;
|
timerId = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content-callable message types that should NOT reset the inactivity timer.
|
||||||
|
*
|
||||||
|
* Rationale: a content script reading available autofill candidates is a
|
||||||
|
* passive query — it shouldn't keep the vault alive indefinitely while the
|
||||||
|
* user isn't actually interacting with it.
|
||||||
|
*
|
||||||
|
* Today this is the only known passive read; if a future content message
|
||||||
|
* is also passive, add it here with a one-line justification.
|
||||||
|
*/
|
||||||
|
export const READ_ONLY_CONTENT_CALLABLE: ReadonlySet<string> = new Set([
|
||||||
|
'get_autofill_candidates',
|
||||||
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user