Fix race condition: vault:unlocked message lost when service worker responds before content.js loads
Two related races caused substitution to silently fail with at-rest encryption:
1. vault:request-unlock was sent at the top of init(), but the runtime.onMessage
listener that handles the vault:unlocked response was registered only after
await-ing the document-scanner script. A warm service worker (e.g. freshly
woken by a sync operation) could respond before the listener existed, dropping
the message permanently.
2. Even if the listener was registered in time, it posted to window immediately,
but content.js hadn't been injected yet, so its window.addEventListener('message')
handler wasn't live and the message went nowhere.
Fix: register api.runtime.onMessage before injecting content.js, and move the
vault:request-unlock send into script.onload — by that point content.js has fully
executed and its message listener is live.
Also stop passing the encrypted settings blob as initial config. When isLocked,
ss_settings is { _ssLocalEncrypted: true, data: '...' }; passing it to content.js
as the initial settings object clutters the settings with encrypted garbage.
Now falls back to { enabled: true } like mappings/identity already did.
https://claude.ai/code/session_01CwcZK8nqL8pyBH9AxDs9qo
This commit is contained in:
+40
-31
@@ -58,20 +58,14 @@
|
||||
// Load mappings and settings, then inject into page
|
||||
async function init() {
|
||||
const result = await api.storage.local.get(['ss_mappings', 'ss_identity', 'ss_settings']);
|
||||
const settings = result.ss_settings || { enabled: true };
|
||||
|
||||
// Check if data is encrypted — pass empty config and request decryption
|
||||
// The background will send decrypted data via vault:unlocked when ready
|
||||
// Check if data is encrypted — pass empty config; decrypted data arrives via vault:unlocked
|
||||
const isLocked = result.ss_mappings?._ssLocalEncrypted ||
|
||||
result.ss_identity?._ssLocalEncrypted;
|
||||
result.ss_identity?._ssLocalEncrypted ||
|
||||
result.ss_settings?._ssLocalEncrypted;
|
||||
const mappings = isLocked ? [] : (result.ss_mappings || []);
|
||||
const identityData = isLocked ? {} : (result.ss_identity || {});
|
||||
|
||||
// If data is encrypted but the vault key may already be cached
|
||||
// (e.g. after a sync import), ask the background to push decrypted data
|
||||
if (isLocked) {
|
||||
api.runtime.sendMessage({ type: 'vault:request-unlock' }).catch(() => {});
|
||||
}
|
||||
const settings = isLocked ? { enabled: true } : (result.ss_settings || { enabled: true });
|
||||
|
||||
// Merge active profiles into a flat identity object for the content script
|
||||
const identity = mergeProfiles(identityData);
|
||||
@@ -84,12 +78,47 @@
|
||||
await new Promise(resolve => { docScannerScript.onload = resolve; docScannerScript.onerror = resolve; });
|
||||
docScannerScript.remove();
|
||||
|
||||
// Register the runtime message listener BEFORE injecting content.js and BEFORE
|
||||
// sending vault:request-unlock. This prevents two races:
|
||||
// 1. The background could respond to vault:request-unlock before the listener
|
||||
// is registered (if the service worker is already warm), dropping the message.
|
||||
// 2. window.postMessage from the listener must reach content.js's message handler,
|
||||
// which is only registered after content.js finishes executing.
|
||||
// Solution: register the listener here, but send vault:request-unlock only inside
|
||||
// script.onload (after content.js has fully executed).
|
||||
api.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.type === 'settings:updated') {
|
||||
window.postMessage({
|
||||
type: 'ss:config-updated',
|
||||
settings: message.settings,
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Vault unlocked — background sends pre-decrypted data
|
||||
if (message.type === 'vault:unlocked') {
|
||||
window.postMessage({
|
||||
type: 'ss:config-updated',
|
||||
mappings: message.mappings || [],
|
||||
identity: message.identity || {},
|
||||
settings: message.settings || {},
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
|
||||
// Inject the main interception script into the page's world
|
||||
const script = document.createElement('script');
|
||||
script.setAttribute('data-ss-config', JSON.stringify({ mappings, identity, settings }));
|
||||
script.src = api.runtime.getURL('src/content/content.js');
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
script.onload = () => script.remove();
|
||||
script.onload = () => {
|
||||
script.remove();
|
||||
// content.js has fully executed — its window.message listener is now live.
|
||||
// Safe to request decrypted data; the vault:unlocked response will be
|
||||
// delivered to content.js without a race.
|
||||
if (isLocked) {
|
||||
api.runtime.sendMessage({ type: 'vault:request-unlock' }).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for substitution events from the page script
|
||||
window.addEventListener('message', async (event) => {
|
||||
@@ -148,26 +177,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for settings updates and vault unlock from background
|
||||
api.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.type === 'settings:updated') {
|
||||
window.postMessage({
|
||||
type: 'ss:config-updated',
|
||||
settings: message.settings,
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Vault unlocked — background sends pre-decrypted data
|
||||
if (message.type === 'vault:unlocked') {
|
||||
window.postMessage({
|
||||
type: 'ss:config-updated',
|
||||
mappings: message.mappings || [],
|
||||
identity: message.identity || {},
|
||||
settings: message.settings || {},
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
|
||||
// Storage bridge — lets page world script read/write storage
|
||||
window.addEventListener('message', async (event) => {
|
||||
if (event.source !== window) return;
|
||||
|
||||
Reference in New Issue
Block a user