fix(api): explicit scope, audit log and atomic sweep for DELETE /v1/batches/delete-completed (omni-code-review battery on #12969) - #13262
Merged
diegosouzapw merged 4 commits intoSep 10, 2026
Conversation
…eted batches Follow-up to #12969 (GHSA-wvxc-jp3v-5mg5) from the omni-code-review battery (LEDGER-1/2/3/4/7/8/9/10): - deleteCompletedBatches takes an explicit scope `{ apiKeyId } | { allTenants: true }`; a missing/empty id throws instead of silently sweeping the whole instance - route branches on the dashboard session explicitly; keys only sweep their own batches; batches with no owner stay out of a key-scoped sweep on purpose (JSDoc) - every sweep is logged (warn for instance-wide, info for key-scoped); a failing sweep returns a sanitized 500 via buildErrorBody - the file soft-deletes, checkpoint delete and batch delete run in one transaction; the empty catch around deleteFile now logs the failure - route-level regression test through the real handler; test files self-isolate their DATA_DIR so the single-file command never touches ~/.omniroute - changelog.d fragment
… completed batches Round-2 findings of the omni-code-review battery on the previous commit: - a presented API key always scopes the sweep to that key, even when the request also carries a dashboard session cookie (parity with GET /v1/batches; a leaked or over-shared key can never widen a destructive sweep); only a session without a key sweeps the whole instance - both sweep modes log at warn so the audit trail survives APP_LOG_LEVEL=warn; the failure log carries the error stack - a scope carrying both apiKeyId and allTenants is rejected instead of widening - changelog fragment links the PR
…og assertion is environment-independent
…ch sweep; no-op key sweeps log at info
diegosouzapw
merged commit Sep 10, 2026
150ca00
into
fix/batches-delete-completed-authz
3 checks passed
This was referenced Sep 10, 2026
diegosouzapw
added a commit
that referenced
this pull request
Sep 14, 2026
Merged after reconciling the whole stack onto the tip, operator-reviewed before merge. The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via #13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, #13262 (explicit scope, audit log, atomic sweep), #13297 (revoked, deactivated, banned or expired keys rejected) and #13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over #13211's because it is stricter on every point: - **route:** with #13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500. - **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws. - **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content. - **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table. #13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact. - 51/51 across the six batch suites (#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`) - ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean⚠️ base-red inherited: #12732
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…w#12969) Merged after reconciling the whole stack onto the tip, operator-reviewed before merge. The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via diegosouzapw#13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, diegosouzapw#13262 (explicit scope, audit log, atomic sweep), diegosouzapw#13297 (revoked, deactivated, banned or expired keys rejected) and diegosouzapw#13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over diegosouzapw#13211's because it is stricter on every point: - **route:** with diegosouzapw#13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500. - **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws. - **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content. - **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table. diegosouzapw#13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact. - 51/51 across the six batch suites (diegosouzapw#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`) - ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean⚠️ base-red inherited: diegosouzapw#12732
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Findings surfaced by the
/omni-code-reviewbattery over PR #12969 (fix/batches-delete-completed-authz, 3 files) reviewed againstrelease/v3.8.51— 3 Opus reviewers (two-axiscode-review,pr-review-toolkit:silent-failure-hunter,quality-scan), 12 raw findings → 10 ledger entries, owner-approved plan. Ledger and per-reviewer reports:_tasks/reviews/omni-code-review/2026-09-10_test-omni-pr12969_vs_release-v3.8.51/.Resolved
src/lib/db/batches.ts+ route) — the instance-wide sweep was the helper's default (deleteCompletedBatches()/ falsy id). New explicit scope{ apiKeyId } | { allTenants: true }; a missing/empty id throws instead of widening; the route branches onscope.isSessionAuthexplicitly (absorbs LEDGER-5 and LEDGER-6). Tests:batches-delete-completed-ownership-wvxc.test.ts,batch-deletion.test.ts(callers migrated, assertions unchanged).tests/unit/batches-delete-completed-route-scope.test.tsthrough the real handler (realcreateApiKeykeys + jose session cookie): key A cannot sweep key B's batch; a session (even with a bearer key) sweeps the instance; no creds → 401; injected failure → sanitized 500 with nothing deleted. Flip proof: forcing the route to{ allTenants: true }fails the test.api_key_id IS NULLstay out of a key-scoped sweep on purpose (diverges fromscopeCheckin the single-batch route); documented in the JSDoc and pinned by a test.warnfor instance-wide,infofor key-scoped: route, mode, apiKeyId, counts); the helper call is wrapped and a failure returnsbuildErrorBody(500, …)(Hard Rule fix(ui): fix Select dropdown dark theme inconsistency #12) — no stack, no raw SQLite text.catcharounddeleteFilenow logsdeleteCompletedBatches: file soft-delete failedwith the file id; counting unchanged. Scoping the file half by owner is deliberately left for a follow-up.db.transaction(present on all four adapters); test injects aRAISE(ABORT)trigger on the final DELETE and asserts the file content rolls back.DATA_DIRbefore loading@/lib/db/*(top-levelawait import), so running them without theisolateDataDirshim never touches~/.omniroute(verified by sqlite mtimes).changelog.d/fixes/12969-batches-delete-completed-ownership.md.Round 2 (re-review of the fix diff, 3 lenses) — resolved
GET /v1/batchesis key-first. Now a presented key always scopes the sweep; only a session WITHOUT a key sweeps the instance (parity with list/count, least privilege for a destructive path). Test: "a request carrying BOTH a session cookie and an API key is scoped to the key".{ apiKeyId, allTenants: true }silently widened — now rejected ("mutually exclusive") + test.infovanished underAPP_LOG_LEVEL=warn— both sweep modes log atwarn.err.message— now{ message, stack }.APP_LOG_LEVELso its log assertion is environment-independent.Acknowledged / owner decisions (not in this PR)
fileIdsbyapi_key_id = ?for key-scoped sweeps (NULL-owned files left toallTenants).filesFailedin the body).allTenantspath.Validation
list-regression.test.tsx20/20 (vitest).43aba7dd(round-1 fix) on the 32-core box:test:vitest51 files ✅ ·build✅ ·test:unit37 704 pass / 42 fail — 0 related to batches; all 27 failing files fail IDENTICALLY on the PR head3355012(base-vs-fix discriminator, same box) → pre-existing on the branch, not introduced here.becb5954,c82c7bed, 5 files): focused suites 31/31 ✅ (route-scope5,ownership-wvxc7,batch-deletion8,batch-deletion-route-logic11), eslint/prettier/typecheck ✅.changelog-integrity✅ against this PR's base (the local fallback compares withrelease/v3.8.51, 44 commits ahead of the source branch).62a3c815and validated by focused tests (33/33) + eslint/prettier/typecheck, not re-reviewed (round cap):Invalid API key, nothing deleted (test added).info(instance-wide and any sweep that removed rows stay atwarn); JSDoc documents the mixed-scope rejection; the route-test docblock matches the shipped precedence; mixed-scope test split into its own case.mock.moduleunreliable in this harness).release/v3.8.51, not introduced here.