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:
adlee-was-taken
2026-05-30 21:42:44 -04:00
parent 4a1c553f9d
commit ba5d218841
3 changed files with 46 additions and 2 deletions

View File

@@ -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);
});
});

View File

@@ -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) {

View File

@@ -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',
]);