fix: resolve 4 open bugs (#231, #232, #233, #234)#239
Conversation
- #234: Reject extension token messages when sender has no tab URL instead of skipping origin validation - #233: Cache userInfo in chrome.storage.local for offline fallback; fall back to unauthenticated when no cache exists - #232: Atomically mark pending recipients as failed in cancelCampaign instead of returning an optimistic stale count - #231: Add SET LOCAL lock_timeout = '5s' to email.delivered webhook handler, consistent with all other event handlers https://claude.ai/code/session_01Danxnk8GmCxffs8cm5saep
📝 WalkthroughWalkthroughFixes four reliability/security issues: extension token origin validation bypass when sender tab is missing, popup showing authenticated state with null userInfo on network error, campaign cancellation returning stale pending counts, and missing transaction lock_timeout in webhook handlers to prevent indefinite DB locks. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@extension/src/popup/popup.ts`:
- Around line 96-98: The code assigns cached.cachedUserInfo directly to userInfo
after chrome.storage.local.get("cachedUserInfo"); add a defensive shape check
before assignment: implement a small type-guard (e.g., isUserInfo(obj)) that
verifies required properties of the expected UserInfo and only set userInfo =
cached.cachedUserInfo when isUserInfo returns true; otherwise ignore the cache
or clear it. Reference chrome.storage.local.get, cached.cachedUserInfo, and the
userInfo variable when locating where to add the check.
In `@server/services/resendWebhook.ts`:
- Around line 134-136: The transaction in resendWebhook sets a 5s lock_timeout
(see tx.execute(sql`SET LOCAL lock_timeout = '5s'`)) which can surface timeout
errors when cancelCampaign holds campaign_recipients locks; leave the timeout
as-is but add targeted handling: wrap the db.transaction call in resendWebhook
with a try/catch that detects Postgres lock timeout errors (e.g., SQLSTATE 55P03
or message "lock timeout") and increment a monitoring metric / emit a warning
log that includes context (webhook id, campaign id), so retries are observable
without changing cancelCampaign or the SET LOCAL lock_timeout behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 64c0acc6-86ca-4dca-96c6-af2e5e844be6
📒 Files selected for processing (5)
extension/src/background/service-worker.tsextension/src/popup/popup.tsserver/services/campaignEmail.tsserver/services/resendWebhook.test.tsserver/services/resendWebhook.ts
| await db.transaction(async (tx) => { | ||
| await tx.execute(sql`SET LOCAL lock_timeout = '5s'`); | ||
| const [updated] = await tx |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Operational note: Lock contention with cancelCampaign is possible.
When cancelCampaign holds locks on campaign_recipients rows for longer than 5 seconds, this webhook handler will experience a lock timeout error. This is an acceptable tradeoff since:
- The 5s timeout prevents indefinite hangs
- Resend/Svix will retry failed webhook deliveries
- The atomic guards ensure idempotent processing on retry
No action needed, but worth monitoring for elevated lock timeout errors during heavy cancellation activity.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/services/resendWebhook.ts` around lines 134 - 136, The transaction in
resendWebhook sets a 5s lock_timeout (see tx.execute(sql`SET LOCAL lock_timeout
= '5s'`)) which can surface timeout errors when cancelCampaign holds
campaign_recipients locks; leave the timeout as-is but add targeted handling:
wrap the db.transaction call in resendWebhook with a try/catch that detects
Postgres lock timeout errors (e.g., SQLSTATE 55P03 or message "lock timeout")
and increment a monitoring metric / emit a warning log that includes context
(webhook id, campaign id), so retries are observable without changing
cancelCampaign or the SET LOCAL lock_timeout behavior.
…tests - Clear cachedUserInfo from chrome.storage.local on disconnect to prevent stale identity leakage (security review) - Add terminal-status WHERE guard to active-cancel campaign counter update for consistency with finalizeCampaign pattern (architecture review) - Document double-write invariant between cancel and finalizeCampaign - Add tests for active-send cancel path and lock_timeout ordering https://claude.ai/code/session_01Danxnk8GmCxffs8cm5saep
…ache validation - Add WHERE status='pending' guard to sendSingleCampaignEmail success UPDATE to prevent overwriting a 'failed' status set by concurrent cancel - Add SET LOCAL lock_timeout='5s' to active-send cancel transaction - Validate cached userInfo shape before trusting it on network error - Update test mock to account for lock_timeout call in cancel transaction https://claude.ai/code/session_01Danxnk8GmCxffs8cm5saep
Version bump for service-worker origin validation fix and popup offline fallback cache changes. https://claude.ai/code/session_01Danxnk8GmCxffs8cm5saep
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
extension/src/popup/popup.ts (1)
207-214: 🧹 Nitpick | 🔵 TrivialConsider atomicity of disconnect cleanup (minor).
The two
awaitcalls (clearToken()thenchrome.storage.local.remove("cachedUserInfo")) are not atomic. Ifinit()runs concurrently (e.g., from anFTC_AUTH_COMPLETEmessage event), it could briefly see token cleared butcachedUserInfostill present.In practice, this is low risk because
init()checks token validity first, and the fallback to cached userInfo only occurs on network errors — not on token absence. The worst case is a momentary UI flicker before landing on unauthenticated state.🔧 Optional: Atomic cleanup
document.getElementById("disconnect-btn")?.addEventListener("click", async () => { - await clearToken(); - await chrome.storage.local.remove("cachedUserInfo"); + await chrome.storage.local.remove(["ftc_extension_token", "ftc_token_expiry", "cachedUserInfo"]); userInfo = null; dropdownOpen = false; state = "unauthenticated"; render(); });Note: This would require exporting the storage key constants from
token.tsor duplicating them.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extension/src/popup/popup.ts` around lines 207 - 214, The disconnect flow can be non-atomic: clearToken() is awaited then chrome.storage.local.remove("cachedUserInfo") is awaited separately, allowing init() (e.g., triggered by FTC_AUTH_COMPLETE) to observe a cleared token but still-present cachedUserInfo; make the cleanup atomic by removing both token and cachedUserInfo in a single storage operation or by exporting and using the same storage key constant from token.ts so you can call chrome.storage.local.remove([...]) (or chrome.storage.local.set with both keys cleared) in one awaited call; update the click handler that references clearToken() and chrome.storage.local.remove("cachedUserInfo") to use the combined removal approach and ensure userInfo = null, dropdownOpen = false, state = "unauthenticated" and render() run after the atomic operation completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@extension/src/popup/popup.ts`:
- Around line 207-214: The disconnect flow can be non-atomic: clearToken() is
awaited then chrome.storage.local.remove("cachedUserInfo") is awaited
separately, allowing init() (e.g., triggered by FTC_AUTH_COMPLETE) to observe a
cleared token but still-present cachedUserInfo; make the cleanup atomic by
removing both token and cachedUserInfo in a single storage operation or by
exporting and using the same storage key constant from token.ts so you can call
chrome.storage.local.remove([...]) (or chrome.storage.local.set with both keys
cleared) in one awaited call; update the click handler that references
clearToken() and chrome.storage.local.remove("cachedUserInfo") to use the
combined removal approach and ensure userInfo = null, dropdownOpen = false,
state = "unauthenticated" and render() run after the atomic operation completes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5146b126-0b3e-4f32-9876-6e85040a37aa
⛔ Files ignored due to path filters (1)
extension/fetchthechange-extension.zipis excluded by!**/*.zip
📒 Files selected for processing (5)
extension/manifest.jsonextension/src/popup/popup.tsserver/services/campaignEmail.test.tsserver/services/campaignEmail.tsserver/services/resendWebhook.test.ts
All findings addressed in commits aad7490-6e420b3.
The branch claude/fix-issue-eWlh3 bumped the extension version to 1.0.1, but main already has manifest.json at 1.0.2. This syncs package.json to match. The other changes on that branch (regressions to auth validation, campaign email race condition fixes, and webhook lock timeouts) were all superseded by PRs #239, #241, and #245. https://claude.ai/code/session_019h1ZcdmqQm4mE8EQFRA2bg
Summary
sender.tab?.urlis undefined, closing the origin validation bypassuserInfoinchrome.storage.localafter successful verification; restore it on network error so tier/account display correctly offline; fall back to unauthenticated when no cache existscancelCampaignfor active sends, atomically mark pending recipients as failed in the database instead of returning an optimistic stalependingCountSET LOCAL lock_timeout = '5s'to theemail.deliveredwebhook handler, consistent withemail.opened,email.clicked,email.bounced, andemail.complainedTest plan
npm run checkpassesnpm run test— all 1692 tests pass (updated 2 resendWebhook tests for new lock_timeout call)FTC_EXTENSION_TOKENfrom extension console withoutsender.tab→ should get{ ok: false, error: "no sender tab" }cancelledcount should match DB rows actually marked as failedCloses #231, closes #232, closes #233, closes #234
https://claude.ai/code/session_01Danxnk8GmCxffs8cm5saep
Summary by CodeRabbit
Bug Fixes
Performance & Reliability
Tests
Chore