Skip to content

fix(rest): enforce per-object API access on the cross-object batch route (#1604)#3229

Merged
os-zhuang merged 4 commits into
mainfrom
claude/cross-object-atomic-batch-write-2k04db
Jul 18, 2026
Merged

fix(rest): enforce per-object API access on the cross-object batch route (#1604)#3229
os-zhuang merged 4 commits into
mainfrom
claude/cross-object-atomic-batch-write-2k04db

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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}/batch route 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}/batch wraps 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:

  • write to an object with enable.apiEnabled: false (hidden from the API), or
  • run an operation outside an object's enable.apiMethods whitelist,

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)

  • Per-object API access on every op. enable.apiEnabled / enable.apiMethods are 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. enforceApiAccess was refactored to share a pure apiAccessDenialFromEnable check + a loadObjectItems helper with the batch route — single-record behavior is unchanged (covered by the existing rest.test.ts).
  • Zod-First request contract. New CrossObjectBatchRequestSchema / CrossObjectBatchOperationSchema / CrossObjectBatchResponseSchema in @objectstack/spec/api; the route validates the body against it, so a malformed op / unknown action / missing object is a 400, not a 500.
  • Honest edges: update/delete require an id (400); an unresolvable { $ref } is 400 BATCH_UNRESOLVED_REF instead of a silently-written null FK; an explicit atomic: false is rejected (400 BATCH_NOT_ATOMIC) rather than silently applied atomically (non-atomic per-object batches stay on POST /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, $ref resolution, atomic rollback surfacing, API-access denial 404/405, and request validation 400s). Engine-level atomicity remains covered by engine-ambient-transaction.test.ts.

Verified locally: @objectstack/rest 323 passed (incl. 15 new), @objectstack/spec batch 25 passed, @objectstack/objectql ambient-tx 4 passed; rest + spec build clean.

ObjectUI

No change needed — the masterDetailTxdataSource.batchTransactionPOST /api/v1/batch wiring already exists and is compatible: it always supplies an id for update/delete ops and only sends the four contract fields.

🤖 Generated with Claude Code

https://claude.ai/code/session_015DgfsaEfmwVAY1SsPtQJ6U


Generated by Claude Code

…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
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 18, 2026 4:31pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec.

102 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/rest, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
claude added 2 commits July 18, 2026 15:55
…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
os-zhuang marked this pull request as ready for review July 18, 2026 16:03
@os-zhuang
os-zhuang merged commit 43a3efb into main Jul 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/cross-object-atomic-batch-write-2k04db branch July 18, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants