Skip to content

fix: stop /updates from serving deleted bundles#1879

Open
riderx wants to merge 1 commit intomainfrom
fix/advisory-ghsa-hqq2-deleted-bundle-updates
Open

fix: stop /updates from serving deleted bundles#1879
riderx wants to merge 1 commit intomainfrom
fix/advisory-ghsa-hqq2-deleted-bundle-updates

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 31, 2026

Summary (AI generated)

  • exclude deleted app_versions from both /updates channel-selection joins
  • send the service-role Authorization header from supabaseAdmin so trusted internal channel updates keep working with the new immutability trigger
  • add an /updates regression for deleted bundles and update channel-setting tests to use the supported API path

Motivation (AI generated)

Deleting a bundle should immediately make it ineligible for OTA selection. Without the deleted filter, devices can still resolve a channel to a soft-deleted bundle as long as the channel still points at that version row.

Business Impact (AI generated)

This closes a stale-release security gap: when an operator deletes a bad or sensitive bundle, Capgo now stops offering it through /updates instead of continuing to route devices toward it.

Test Plan (AI generated)

  • bunx eslint "supabase/**/*.{ts,js}" tests/updates.test.ts
  • bun run supabase:with-env -- bunx vitest run tests/updates.test.ts
  • bun run test:all

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Deleted bundles are no longer incorrectly resolved when referenced by active channels.
  • Tests

    • Added test coverage for deleted bundle handling in channel update scenarios.

@riderx riderx added the codex label Mar 31, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

The pull request adds version deletion filtering to channel/device info queries, refactors the Supabase admin client initialization to explicitly handle the service role key, and expands test coverage with a new test for deleted bundle handling while refactoring existing update scenario tests using a shared helper function.

Changes

Cohort / File(s) Summary
Database Query Filtering
supabase/functions/_backend/utils/pg.ts
Added versionAlias.deleted = false condition to join clauses in requestInfosChannelDevicePostgres and requestInfosChannelPostgres to exclude deleted versions from query results.
Supabase Client Configuration
supabase/functions/_backend/utils/supabase.ts
Refactored supabaseAdmin() to explicitly extract SUPABASE_SERVICE_ROLE_KEY into a local variable and pass it as both the auth key and in the Authorization header for the Supabase client.
Test Suite Expansion & Refactoring
tests/updates.test.ts
Introduced updateChannel() helper function for channel updates with automatic version resolution. Added new test case validating that deleted bundles referenced by channels are not resolved. Refactored multiple existing "update scenarios" tests to use the new helper, reducing boilerplate and improving consistency.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Deleted versions fade from sight,
Queries filter with delight!
Helper functions make tests bright,
Bundles checked, logic tight! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes a comprehensive summary with motivation and business impact, but lacks explicit test plan steps and checklist items required by the template. Replace the AI-generated description with explicit steps to reproduce the test plan and check off the required checklist items (code style, documentation, test coverage, manual testing).
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: stop /updates from serving deleted bundles' directly and clearly describes the main security fix in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/advisory-ghsa-hqq2-deleted-bundle-updates

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 31, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing fix/advisory-ghsa-hqq2-deleted-bundle-updates (b004950) with main (3fd1ec7)

Open in CodSpeed

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b004950977

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.from(channelDevicesAlias)
.innerJoin(channelAlias, eq(channelDevicesAlias.channel_id, channelAlias.id))
.innerJoin(versionAlias, eq(channelAlias.version, versionAlias.id))
.innerJoin(versionAlias, and(eq(channelAlias.version, versionAlias.id), eq(versionAlias.deleted, false)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid fallback to public channel when override bundle is deleted

Filtering channel_devices with eq(versionAlias.deleted, false) makes an override row disappear entirely when its channel points to a soft-deleted bundle. In the /updates flow, updateWithPG treats a missing override as "no override" and falls back to the default/public channel selection, so devices pinned to a private or staged channel can suddenly be routed to another channel’s release after bundle deletion. This is a behavior regression introduced by this join change; deleted override targets should produce an explicit non-update/no-channel result instead of implicit fallback routing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/updates.test.ts (1)

689-718: ⚠️ Potential issue | 🟡 Minor

Wrap the shared production channel cleanup in try/finally.

These cases restore production only after the assertions pass. If either test exits early, later cases inherit the private/no-self-set state and start failing for the wrong reason.

As per coding guidelines, "Create dedicated seed data for tests that modify shared resources; do not reuse seed data if your test modifies it."

Also applies to: 720-777

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/updates.test.ts` around lines 689 - 718, The test mutates the shared
'production' channel and currently restores it only after assertions, so wrap
the channel state changes and assertions in a try/finally: call
updateChannel('production', {...public:false...}) before the try, run
postUpdate/getBaseData and all expects inside the try, and move the restoration
updateChannel('production', {public:true, allow_device_self_set:true}) into the
finally block to guarantee cleanup; apply the same try/finally pattern to the
similar test block around lines 720-777 to ensure shared state is always
restored.
🧹 Nitpick comments (1)
tests/updates.test.ts (1)

102-156: Consider a channel_devices variant of this regression too.

This new case only protects the default-channel lookup. A future regression in the device-override join would still slip through unless that path is covered elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/updates.test.ts` around lines 102 - 156, Add a second test variant that
covers the device-override path: in the existing test "does not resolve deleted
bundles that are still referenced by a channel" create a device id (e.g.,
randomUUID()), insert a record into the channel_devices table linking that
device_id to the same channelName (use the same fields as channels insert but
just channel_id/name and device_id), then call postUpdate with the same baseData
plus the device identifier field used by postUpdate (e.g., device_id or device)
and assert the response.status is 200 and json.error === 'no_channel' and
json.version is undefined; this ensures the device-override join path
(channel_devices) is covered in addition to the default-channel lookup.
🤖 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 `@tests/updates.test.ts`:
- Around line 689-718: The test mutates the shared 'production' channel and
currently restores it only after assertions, so wrap the channel state changes
and assertions in a try/finally: call updateChannel('production',
{...public:false...}) before the try, run postUpdate/getBaseData and all expects
inside the try, and move the restoration updateChannel('production',
{public:true, allow_device_self_set:true}) into the finally block to guarantee
cleanup; apply the same try/finally pattern to the similar test block around
lines 720-777 to ensure shared state is always restored.

---

Nitpick comments:
In `@tests/updates.test.ts`:
- Around line 102-156: Add a second test variant that covers the device-override
path: in the existing test "does not resolve deleted bundles that are still
referenced by a channel" create a device id (e.g., randomUUID()), insert a
record into the channel_devices table linking that device_id to the same
channelName (use the same fields as channels insert but just channel_id/name and
device_id), then call postUpdate with the same baseData plus the device
identifier field used by postUpdate (e.g., device_id or device) and assert the
response.status is 200 and json.error === 'no_channel' and json.version is
undefined; this ensures the device-override join path (channel_devices) is
covered in addition to the default-channel lookup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f46a18b7-fec9-4f3a-bb1b-9d3a8aab820f

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd1ec7 and b004950.

📒 Files selected for processing (3)
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/supabase.ts
  • tests/updates.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant