fix(rest): enforce per-object API access on the cross-object batch route (#1604)#3229
Merged
Merged
Conversation
…ute (#1604) The POST {basePath}/batch cross-object transactional batch (issue #1604 / ADR-0034) wraps N create/update/delete ops in one engine transaction but skipped the per-object API-exposure gate every single-record write applies — so an authenticated caller could write to an apiEnabled:false object, or run an operation outside an object's apiMethods whitelist, straight through the batch surface (ADR-0049 / #1889; the same declared-not-enforced hole closed for the generic write path in #3220 / #3213). - Validate the request against a new CrossObjectBatchRequestSchema (@objectstack/spec/api, Zod-First); a malformed op / unknown action / missing object is now a 400, not a 500. - Enforce enable.apiEnabled / apiMethods for EVERY op (metadata fetched once, each distinct object+action checked once) BEFORE opening the transaction → 404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED. - Require an id for update/delete; reject an unresolvable {$ref} with 400 BATCH_UNRESOLVED_REF instead of writing a silent null FK; reject an explicit atomic:false (400 BATCH_NOT_ATOMIC). - Refactor enforceApiAccess to share the pure apiAccessDenialFromEnable check + a loadObjectItems helper with the batch route (single-record behavior unchanged). - Add rest-batch-endpoint.test.ts — the REST-boundary coverage ADR-0034 flagged as missing (commit, $ref, rollback surfacing, API-access denial, validation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 102 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…cs reference CI follow-up on the cross-object batch hardening (#1604): - The per-op (object, action) dedup key in the /batch handler used a raw NUL (0x00) byte as its separator, which trips the check:nul-bytes gate (a raw NUL makes the file read as binary to grep/ripgrep). Replaced with the standard unicode NUL escape sequence, matching the convention already used for the exec-ctx memo key elsewhere in rest-server.ts. Byte-identical at runtime. - Regenerated content/docs/references/api/batch.mdx (generated from the Zod spec) so it documents the new CrossObjectBatch* schemas — the check:docs gate requires the reference to track packages/spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U
…ports
check:api-surface flagged 6 additive public exports (CrossObjectBatch{Operation,Request,Response} + their schemas) from #1604 — 0 breaking, 6 added. Regenerate the committed snapshot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U
…nt + error semantics Keep the hand-written batch-endpoint docs honest about the behavior added in this PR (Prime Directive #10): per-object API-exposure gate (404/405), request validation (400), unresolvable $ref (400 BATCH_UNRESOLVED_REF), and atomic-only (400 BATCH_NOT_ATOMIC). Also list the cross-object POST /batch row in the implementation-status endpoint table. Generated reference (api/batch.mdx) is regenerated separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U
os-zhuang
marked this pull request as ready for review
July 18, 2026 16:03
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.
Summary
Closes the security gap that kept the cross-object atomic batch (issue #1604 / ADR-0034) from being shippable. The engine foundation (ambient transaction, ADR-0034) and the
POST {basePath}/batchroute already exist and work; the endpoint's own "dedicated reviewed change" caveat surfaced one real hole and some rough edges, fixed here.The gap
POST {basePath}/batchwraps N cross-object create/update/delete ops in one engine transaction, but — unlike every single-record write route — it skipped the per-object API-exposure gate (enforceApiAccess). An authenticated caller could therefore:enable.apiEnabled: false(hidden from the API), orenable.apiMethodswhitelist,straight through the batch surface. This is the same "declared ≠ enforced" hole (ADR-0049 / #1889) recently closed for the generic write path in #3220 / #3213 — the batch route was the one remaining bypass.
What changed (
packages/rest,packages/spec)enable.apiEnabled/enable.apiMethodsare now enforced for each operation before the transaction is opened —404 OBJECT_API_DISABLED/405 OBJECT_API_METHOD_NOT_ALLOWED. Object metadata is fetched once and each distinct(object, action)checked once.enforceApiAccesswas refactored to share a pureapiAccessDenialFromEnablecheck + aloadObjectItemshelper with the batch route — single-record behavior is unchanged (covered by the existingrest.test.ts).CrossObjectBatchRequestSchema/CrossObjectBatchOperationSchema/CrossObjectBatchResponseSchemain@objectstack/spec/api; the route validates the body against it, so a malformed op / unknown action / missingobjectis a400, not a500.update/deleterequire anid(400); an unresolvable{ $ref }is400 BATCH_UNRESOLVED_REFinstead of a silently-writtennullFK; an explicitatomic: falseis rejected (400 BATCH_NOT_ATOMIC) rather than silently applied atomically (non-atomic per-object batches stay onPOST /data/:object/batch).Tests
Adds
packages/rest/src/rest-batch-endpoint.test.ts— the REST-boundary coverage ADR-0034 explicitly flagged as missing (multi-op commit,$refresolution, atomic rollback surfacing, API-access denial404/405, and request validation400s). Engine-level atomicity remains covered byengine-ambient-transaction.test.ts.Verified locally:
@objectstack/rest323 passed (incl. 15 new),@objectstack/specbatch 25 passed,@objectstack/objectqlambient-tx 4 passed;rest+specbuild clean.ObjectUI
No change needed — the
masterDetailTx→dataSource.batchTransaction→POST /api/v1/batchwiring already exists and is compatible: it always supplies anidfor update/delete ops and only sends the four contract fields.🤖 Generated with Claude Code
https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U
Generated by Claude Code