Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
],
"content_scripts": [
{
"matches": ["*://web.whatsapp.com/*"],
"js": ["content.js"],
"matches": [
"*://web.whatsapp.com/*"
],
"js": [
"content.js"
],
"run_at": "document_idle",
"all_frames": false,
"type": "module"
Expand All @@ -41,5 +45,8 @@
"32": "icon-32.png",
"48": "icon-48.png",
"128": "icon-128.png"
}
},
"optional_host_permissions": [
"https://*/*"
]
}
19 changes: 19 additions & 0 deletions extension/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,26 @@ import { createRouter } from './src/lib/router.js';
* Pure handler used by the Save button. Exposed for testing.
* @param {{ serverUrl:string, apiKey:string }} input
*/

/**
* @param {string} serverUrl
* @returns {Promise<void>}
*/
async function ensureHostPermission(serverUrl) {
const trimmed = String(serverUrl || '').trim();
if (!trimmed) return;
const u = new URL(trimmed);
const pattern = `${u.origin}/*`;
const has = await chrome.permissions.contains({ origins: [pattern] });
if (has) return;
const granted = await chrome.permissions.request({ origins: [pattern] });
if (!granted) {
throw new Error(`Host permission was not granted for ${u.origin}`);
}
}

export async function applyServerForm(input) {
await ensureHostPermission(input.serverUrl);
await saveConfig({ serverUrl: input.serverUrl, apiKey: input.apiKey });
Comment on lines +27 to 28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate server URL before requesting host permission

applyServerForm now calls ensureHostPermission before saveConfig, so parseable-but-invalid inputs (for example https://api.example.com/v1, which saveConfig rejects because only origin or /api paths are allowed) can still trigger and grant a host-permission prompt before the save fails. That leaves an unnecessary persistent permission on the extension even though the configuration was not accepted, which is both confusing and a permission-scope regression for users who mistype URLs.

Useful? React with 👍 / 👎.

await chrome.runtime.sendMessage({ type: 'CONFIG_UPDATED' }).catch(() => {});
}
Expand Down
15 changes: 15 additions & 0 deletions extension/tests/integration/popup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ describe('popup form handlers', () => {
await expect(applyServerForm({ serverUrl: 'not a url', apiKey: 'k' })).rejects.toThrow();
});



it('applyServerForm requests host permission for non-default HTTPS origins', async () => {
chrome.permissions.contains.mockResolvedValue(false);
await applyServerForm({ serverUrl: 'https://radar.example.com', apiKey: 'k' });
expect(chrome.permissions.request).toHaveBeenCalledWith({ origins: ['https://radar.example.com/*'] });
});

it('applyServerForm fails when host permission is denied', async () => {
chrome.permissions.contains.mockResolvedValue(false);
chrome.permissions.request.mockResolvedValue(false);
await expect(applyServerForm({ serverUrl: 'https://radar.example.com', apiKey: 'k' }))
.rejects.toThrow(/host permission/i);
});

it('outgoing payload contains eventVersion', async () => {
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
Expand Down
4 changes: 4 additions & 0 deletions extension/tests/mocks/chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export function createChromeMock() {
},
},
},
permissions: {
contains: vi.fn(async ({ origins }) => origins.every((origin) => origin.startsWith('http://localhost') || origin.startsWith('http://127.0.0.1') || origin.startsWith('https://web.whatsapp.com'))),
request: vi.fn(async () => true),
},
Comment on lines +66 to +69
tabs: {
query: vi.fn(async () => []),
sendMessage: vi.fn(async () => undefined),
Expand Down
Loading