Skip to content

[codex] Harden encrypted bundle update invariant#2015

Merged
riderx merged 9 commits intomainfrom
codex/fix-encrypted-bundle-update-invariant
May 6, 2026
Merged

[codex] Harden encrypted bundle update invariant#2015
riderx merged 9 commits intomainfrom
codex/fix-encrypted-bundle-update-invariant

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented May 2, 2026

Summary (AI generated)

  • Hardened encrypted-bundle database enforcement for direct app_versions updates.
  • Added a regression test for an app-scoped all API key trying to invalidate session_key through PostgREST.

Motivation (AI generated)

The security advisory GHSA-4qwf-mrgx-mvfx reported that encrypted-bundle enforcement was bypassable after insert by directly clearing app_versions.session_key. This keeps the invariant enforced on update as well.

Business Impact (AI generated)

This preserves an organization administrator's encrypted-bundle policy and prevents scoped API keys from weakening OTA bundle protection metadata after upload.

Test Plan (AI generated)

  • bunx eslint tests/enforce-encrypted-bundles.test.ts
  • bun run lint:backend
  • sqlfluff lint supabase/migrations/20260502134012_harden_encrypted_bundle_update_invariant.sql --dialect postgres
  • git diff --check
  • Pre-commit: bun run cli:build && vue-tsc --noEmit
  • GitHub CI: Run tests, Run Playwright tests, CodeQL, SonarCloud, Socket, CodSpeed; Cloudflare backend failed once on unrelated flakiness and passed on rerun

Generated with AI

Summary by CodeRabbit

  • New Features

    • Added a database trigger to enforce encrypted-bundle invariants on inserts/updates and tightened its execution permissions.
  • Bug Fixes

    • Stronger enforcement preventing edits to encrypted-bundle fields once a bundle is marked ready.
    • Rejects unencrypted session keys when an org requires encryption and enforces presence/matching of the required encryption key ID.
  • Tests

    • Expanded coverage and helpers for bundle enforcement, scoped API-key scenarios, manifest/version flows, and trigger-controlled test fixtures.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a PostgreSQL trigger function public.check_encrypted_bundle_on_insert() and enforce_encrypted_bundle_trigger on public.app_versions to enforce encrypted-bundle invariants (ready-state immutability, session_key must be encrypted, optional org-level required key). Also updates tests, test helpers, and a test fixture to exercise and accommodate the trigger.

Changes

Encrypted Bundle Validation Trigger

Layer / File(s) Summary
Function header & variables
supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql
Creates/Replaces public.check_encrypted_bundle_on_insert() (PL/pgSQL, SECURITY DEFINER, empty search_path) and declares org_id, org_enforcing, org_required_key, bundle_is_encrypted, bundle_key_id, bundle_was_ready.
Ready-state protection
.../20260505193449_harden_encrypted_bundle_update_invariant.sql
On UPDATE computes bundle_was_ready and raises bundle_already_ready if ready bundle content fields are altered.
Org resolution & policy load
.../20260505193449_harden_encrypted_bundle_update_invariant.sql
Derives org_id by looking up NEW.app_id in public.apps, falling back to NEW.owner_org; returns NEW early if no org found; loads enforce_encrypted_bundles and required_encryption_key from public.orgs.
Encryption & key validation
.../20260505193449_harden_encrypted_bundle_update_invariant.sql
Checks NEW.session_key with is_bundle_encrypted() and raises encryption_required if not encrypted. If org requires a specific key, ensures NEW.key_id exists and matches required key (prefix/length-tolerant); logs and raises on missing/mismatch.
Trigger creation & privileges
.../20260505193449_harden_encrypted_bundle_update_invariant.sql
Drops existing trigger, creates enforce_encrypted_bundle_trigger BEFORE INSERT OR UPDATE on public.app_versions for listed columns; sets function owner to postgres; revokes public/anon/authenticated privileges; grants EXECUTE to service_role.
Tests / Fixture wiring
tests/enforce-encrypted-bundles.test.ts, supabase/tests/13_test_plan_math.sql
Adds tests exercising enforcement (direct insert/update, CLI completion, app-scoped API key behavior), seeds a scoped API key for tests; wraps a fixture UPDATE with DISABLE/ENABLE TRIGGER in 13_test_plan_math.sql.

Tests, Helpers & Versioning Flow

Layer / File(s) Summary
Test helper signature
tests/test-utils.ts
createAppVersions gains optional values parameter and spreads ...values into the upsert payload to allow injecting extra fields (e.g., external_url, null r2_path).
Version creation & production helpers
tests/updates-manifest.test.ts
Adds createVersionWithoutR2Path(versionName) and setProductionVersion(appVersionId) helpers; uses them to create versions with r2_path = NULL and mark them production for manifest tests.
Test cleanup & scaffolding
tests/updates-manifest.test.ts
Tracks createdVersionIds, deletes manifests for created versions, resets manifest counts, and restores production channel in afterEach.
Tests updated to use helpers
tests/updates.test.ts, tests/updates-manifest.test.ts
Multiple tests now pass external_url at creation via createAppVersions and use the new manifest/version helpers.
Scoped API key utilities & cleanup
tests/files-security.test.ts, tests/enforce-encrypted-bundles.test.ts
Introduces createScopedKey(appId, name, mode) and adapts seeding/cleanup to handle multiple scoped keys and their IDs; tests use scoped keys (upload/all) and explicit Authorization headers where needed.
Cross-tenant subkey test adjustment
tests/app.test.ts
Replaces an API-based cross-tenant subkey creation with a direct DB insert for subkey ownership testing and adjusts cleanup to delete via DB.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant API as API
  participant DB as DB
  participant Apps as "public.apps"
  participant Orgs as "public.orgs"

  Client->>API: INSERT/UPDATE app_versions (NEW)
  API->>DB: SQL executes -> BEFORE trigger fires
  DB->>Apps: SELECT org_id FROM public.apps WHERE id = NEW.app_id
  Apps-->>DB: return org_id (or null)
  DB->>Orgs: SELECT enforce_encrypted_bundles, required_encryption_key FROM public.orgs WHERE id = org_id
  Orgs-->>DB: return policy
  DB->>DB: evaluate bundle_was_ready & is_bundle_encrypted(NEW.session_key)
  DB->>DB: if required key -> compare NEW.key_id with required prefix/length-tolerant match
  alt validation fails
    DB-->>API: RAISE EXCEPTION (encryption_required / key_mismatch / bundle_already_ready)
    API-->>Client: error
  else validation passes
    DB-->>API: RETURN NEW
    API-->>Client: success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Cap-go/capgo#1817: Modifies encrypted-bundle enforcement on public.app_versions and the same trigger/function surface.

Suggested labels

codex

Poem

🐰 I hopped into the DB with a careful stare,
I guard each bundle with a watchful care.
No key — no change; once ready, stay tight,
Triggers hum at dusk and banish the night.
Hop, patch, and validate — all snug and right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] Harden encrypted bundle update invariant' clearly and concisely describes the main change: adding enforcement to prevent bypassing encrypted-bundle checks during UPDATE operations.
Description check ✅ Passed The pull request description includes summary, motivation, business impact, and a comprehensive test plan covering linting, backend checks, and CI validation. It adequately documents the security fix and regression testing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-encrypted-bundle-update-invariant

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 2, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/fix-encrypted-bundle-update-invariant (fa0cca2) with main (afa666f)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review May 2, 2026 14:19
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@riderx riderx force-pushed the codex/fix-encrypted-bundle-update-invariant branch from b1fa38e to 365b15a Compare May 5, 2026 17:32
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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/enforce-encrypted-bundles.test.ts (1)

445-492: ⚡ Quick win

Reset org enforcement in finally blocks for these cases.

Each test enables enforce_encrypted_bundles on the shared org and disables it only on the success path. If an assertion or insert fails early, the rest of the file inherits the wrong org state and starts failing for secondary reasons. Wrapping the enable/disable pair in try/finally would keep failures local.

Also applies to: 494-545, 547-611, 613-668

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/enforce-encrypted-bundles.test.ts` around lines 445 - 492, The test
enables org-level enforcement via getSupabaseClient().from('orgs').update({
enforce_encrypted_bundles: true }) but only disables it on the success path,
which can leak state when assertions or inserts fail; wrap the enable/disable
pair in a try/finally in the test "should reject direct update that changes
session_key after the bundle is ready" (and the other affected tests at the
ranges noted) so that after enabling with ORG_ID_ENCRYPTED you always reset
enforce_encrypted_bundles to false in the finally block; locate the setup calls
using ORG_ID_ENCRYPTED and getSupabaseClient()/from('orgs') and move the
disabling update into finally to guarantee state cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql`:
- Around line 52-61: Current logic trusts NEW.owner_org before consulting the
apps table which can be stale due to trigger ordering; change the resolution
order so the code first SELECTs apps.owner_org INTO org_id FROM public.apps
WHERE apps.app_id = NEW.app_id, and only if that SELECT yields NULL (no matching
app) fall back to using NEW.owner_org; update the block that assigns org_id (the
IF/ELSE around NEW.owner_org and the SELECT) accordingly so org_id is derived
from NEW.app_id first and reference force_valid_owner_org_app_versions in the
comment if helpful.

In `@tests/enforce-encrypted-bundles.test.ts`:
- Around line 149-153: Don't mutate the shared seeded API key (APIKEY_ENCRYPTED)
in the suite setup/teardown; instead, use getSupabaseClient() to INSERT a
dedicated test API key row (set limited_to_apps to [APP_NAME_ENCRYPTED]) for
this test file and store that new key value in a suite-local constant, use that
constant in assertions, and DELETE that row in afterAll to clean up; also
convert tests in this file to it.concurrent() where appropriate so files can run
in parallel. Ensure you reference the created fixture key rather than updating
APIKEY_ENCRYPTED and remove the update calls shown calling
.from('apikeys').update(...) .

---

Nitpick comments:
In `@tests/enforce-encrypted-bundles.test.ts`:
- Around line 445-492: The test enables org-level enforcement via
getSupabaseClient().from('orgs').update({ enforce_encrypted_bundles: true }) but
only disables it on the success path, which can leak state when assertions or
inserts fail; wrap the enable/disable pair in a try/finally in the test "should
reject direct update that changes session_key after the bundle is ready" (and
the other affected tests at the ranges noted) so that after enabling with
ORG_ID_ENCRYPTED you always reset enforce_encrypted_bundles to false in the
finally block; locate the setup calls using ORG_ID_ENCRYPTED and
getSupabaseClient()/from('orgs') and move the disabling update into finally to
guarantee state cleanup.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7c3a6da-2768-4acd-8bdd-11e3c907b04c

📥 Commits

Reviewing files that changed from the base of the PR and between d5e8a5d and 9924776.

📒 Files selected for processing (2)
  • supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql
  • tests/enforce-encrypted-bundles.test.ts

Comment thread supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql Outdated
Comment thread tests/enforce-encrypted-bundles.test.ts Outdated
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.

🧹 Nitpick comments (1)
supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql (1)

110-114: 💤 Low value

Simplify the key comparison logic.

The second condition is redundant: when org_required_key is 20 chars, it duplicates condition 1; when 21 chars, it can never match since bundle_key_id is always 20 chars. Since the database constraint ensures required_encryption_key is exactly 20 or 21 chars, the comparison can be simplified to just check the first 20 characters of the org key:

IF NOT bundle_key_id = LEFT(org_required_key, 20) THEN
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql`
around lines 110 - 114, The existing IF condition compares bundle_key_id against
org_required_key with two branches; simplify it by replacing the OR expression
with a single comparison that checks the first 20 characters of
org_required_key: use bundle_key_id = LEFT(org_required_key, 20) (remove the
LEFT(bundle_key_id, LENGTH(org_required_key)) branch and LENGTH usage) so the IF
becomes IF NOT bundle_key_id = LEFT(org_required_key, 20) THEN, relying on the
enforced 20/21 char invariant of org_required_key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql`:
- Around line 110-114: The existing IF condition compares bundle_key_id against
org_required_key with two branches; simplify it by replacing the OR expression
with a single comparison that checks the first 20 characters of
org_required_key: use bundle_key_id = LEFT(org_required_key, 20) (remove the
LEFT(bundle_key_id, LENGTH(org_required_key)) branch and LENGTH usage) so the IF
becomes IF NOT bundle_key_id = LEFT(org_required_key, 20) THEN, relying on the
enforced 20/21 char invariant of org_required_key.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e814ac91-0ebf-4555-b7b1-0ff102822a39

📥 Commits

Reviewing files that changed from the base of the PR and between a681683 and 1a094a7.

📒 Files selected for processing (4)
  • supabase/migrations/20260505193449_harden_encrypted_bundle_update_invariant.sql
  • tests/app.test.ts
  • tests/enforce-encrypted-bundles.test.ts
  • tests/files-security.test.ts

…undle-update-invariant

# Conflicts:
#	tests/files-security.test.ts
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 6, 2026

@riderx riderx merged commit 39a95fc into main May 6, 2026
38 checks passed
@riderx riderx deleted the codex/fix-encrypted-bundle-update-invariant branch May 6, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant