Build a browser helper for municipal permit reviewers. A reviewer presses the toolbar control while viewing case 47. The helper checks the tab origin and user action, requests only the needed host access, then injects a content script that extracts the case ID, due date, and status from one known page container. A denied grant still leaves a manual case-ID path. The helper does not inspect unrelated tabs or send full page HTML to a service. The content adapter validates one unambiguous case card, places a small labeled panel with text nodes, and disconnects observers on route change. It cannot change the host form or send a checklist without another explicit action. Messages from the content script to the background handler use a versioned type, request ID, case ID, and revision. The handler checks runtime sender metadata and rejects stale tabs, extra destinations, unknown types, and oversized payloads. It asks the server to authorize the actual case. The popup can close at any time; the background handler may stop while idle. Before starting a checklist write, the helper stores a scoped operation key and later reconciles the server status after restart. A lost response reuses the same key so it cannot make a second checklist. The local store retains only minimal pending metadata and expires it under a policy. Sign-out removes private local data and makes replay require a fresh session. The release review inventories requested browser permissions, supported portal versions, and a rollback action if the adapter fails after a site update.
Project: permit checklist browser helper
Build contract
- An unrelated or lookalike site cannot trigger injection or a checklist.
- An ambiguous DOM extraction fails visibly without a guessed case.
- The background handler recovers one operation after popup close or restart.
- Messages never grant broader server access than the signed-in reviewer has.
Implementation checkpoint
function helperRequestAllowed(message, sender, access) {
return sender.origin === 'https://permits.internal.test' && access.userInvoked &&
access.hostGranted && message.type === 'open-checklist' &&
/^CASE-[0-9]{2,6}$/.test(message.caseId) && Number.isInteger(message.revision) &&
message.revision >= 0 && typeof message.requestId === 'string' &&
message.requestId.length > 0 && message.requestId.length <= 96 &&
JSON.stringify(message).length <= 2048 &&
Object.keys(message).every(key => ['type', 'caseId', 'revision', 'requestId'].includes(key));
}
console.log(helperRequestAllowed({ type: 'open-checklist', caseId: 'CASE-47', revision: 29, requestId: 'r-63', uploadTo: 'outside' }, { origin: 'https://permits.internal.test' }, { userInvoked: true, hostGranted: true }));
// Output: falseThe checkpoint rejects a message with an unexpected upload destination. In the actual extension, sender origin and tab identity come from runtime metadata, not a payload field. The message parser should cap encoded size before shape validation. Validation is O(B) for B message bytes, bounded at 2 KiB; a DOM adapter is O(N) over its selected container of N nodes, not the whole document. Pending local operations take O(P) space for P unfinished tasks. Server authorization and idempotency remain independent checks even when all local predicates pass.
Failure drill
Open a similar-looking hostname, deny the optional host grant, add a hidden duplicate case card, change the portal locale, and fire 83 rapid DOM mutations. Confirm the helper either extracts one valid case or stops with a visible manual route, while no duplicate panels or observers remain. Send a forged page message and a content-script message with an extra destination; neither may invoke server work. Close the popup after the server accepts a write but before its response arrives. Restart the browser, reload operation metadata, and reconcile rather than creating a second checklist. Revoke the reviewer on the server before a retry and require denial. Sign out, inspect extension storage, and confirm private page text was not retained. Test keyboard operation, panel focus, and the host form after each injected update.
Acceptance checks
- Permission denial still allows manual case entry.
- One supported page yields one accessible panel and one unambiguous case.
- Worker interruption and response loss do not duplicate a checklist.
- Unexpected message fields and stale senders fail before privileged work.
Common Mistakes
- Requesting all-site access for a user-invoked portal action.
- Trusting extracted page text because it came from an isolated content script.
- Using a background global variable as durable operation state.
Related lessons
Extension Host Permissions and User Invocation; Content Script Isolation and DOM Mutation; Extension Background Events and Durable State; Extension Message Contracts and Page Data Boundary.
