Skip to content

feat: single-source API-method derivation contract (#3391 P1) - #3498

Merged
os-zhuang merged 5 commits into
mainfrom
claude/complete-scheduled-development-xqcwch
Jul 27, 2026
Merged

feat: single-source API-method derivation contract (#3391 P1)#3498
os-zhuang merged 5 commits into
mainfrom
claude/complete-scheduled-development-xqcwch

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

背景

落地 #3391P1 契约(方案见 该 issue 评论,基线 main@69f1dfd5c)。服务端成为唯一裁决者:每个对象的有效操作集由 spec 里唯一一张派生表从 6 个原语(get/list/create/update/delete/bulk)白名单解析,REST gate、runtime dispatcher gate、/me/permissions 注解全部消费同一张表;前端只渲染下发的 effective 结果,不读原始 apiMethods

框架侧三个 PR(方案里的 PR-1/2/3)合并到本分支,按 commit 切分。objectui 侧(PR-4)属独立仓库,不在本分支范围。

改动

PR-1 — 派生真源(d3ae192)

  • @objectstack/spec/data 新增 api-derivation:resolveEffectiveApiMethods / isApiOperationAllowed / effectiveOperationsArray / API_METHOD_DERIVATION / DATA_ACTION_TO_API_OPERATION。三态语义(undefined=全开、[]=全禁、子集=派生闭包);legacy 8 值由 6 原语派生;restore/purge 不派生(enable.trash 已退役 [11.0][A2] Remove dead author-facing metadata properties (ADR-0049 enforce-or-remove) #2377)。
  • runtime api-exposure.ts 重写消费共享表:修复 dispatcher/MCP 死码(getObject() 返回嵌套 .enable,旧扁平读取从未生效)——嵌套优先、兼容扁平;[] 翻转为 deny-all。
  • objectql 注册期 legacy 值 deprecation warning;两张 affordance 表(registry MANAGED_WRITE_VERB_AFFORDANCE、plugin-security WRITE_OP_AFFORDANCE)加交叉引用注释,明确 verb→affordance(UI 意图)与 verb→primitive(API 收紧)两条正交轴。

PR-2 — REST gate 接入(209d3e5)

  • 主 gate apiAccessDenialFromEnable 换共享 resolver;405 allowed 改为 effective 集。
  • import 两段式(粗判先行 + writeMode 精判);export 列投影同 FLS 可读集;bulk 五路统一 bulk ∧ child
  • 四个 in-repo 显式白名单 identity 对象补 bulk 原语。

PR-3 — 有效操作下发(d9941b1)

  • spec EffectiveObjectPermissionSchema(ObjectPermissionSchema.extend({ apiOperations }),仅响应侧,authoring schema 不动);/me/permissions 注解每对象 effective apiOperations,超管 enumerate 补条目;seed → fold → clamp → annotate,全程 guarded。

行为变化(收紧,均属"declared ≠ enforced"缺口收口)

  1. apiMethods: [] + apiEnabled:true → 全 405(in-repo 零影响,[] 对象均配 apiEnabled:false,404 先于 405)。
  2. dispatcher/MCP 白名单由死转活。
  3. import/export 反向派生:CRUD 白名单对象放行 import(⊆create∨update)/export(⊆list);export 表头收缩为 FLS 可读列。
  4. Many/batch 要求 bulk 原语(四对象已补;三方缺 bulk 会 405)。
  5. 405 allowed 由原始白名单改为 effective 集。

测试

  • 新增:spec api-derivation.test.ts(25)、rest rest-api-derivation-gates.test.ts、hono effective-api-operations.test.ts、export FLS 列投影阻塞性两条。
  • 更新:runtime api-exposure.test.ts(翻转 []=deny-all + 嵌套回归)、rest 曝露套件(405 = effective / bulk∧child)、rest-batch-endpoint、spec permission/protocol。
  • 全仓 pnpm build 71/71 通过;涉及包全量测试通过(spec 6880 / runtime 605 / objectql 1073 / rest 372 / hono 107 / plugin-security 540 / platform-objects 215)。

后续(另开 issue,不在本 PR)

用户级 export 权限轴(接 userExportAllowed 槽)、元数据不可解析 fail-open 残余风险、detail/form 面 edit/delete 接 effective、security 服务 getReadableFields 查询面、P2 枚举收缩、objectui 侧接线(PR-4)。

Closes 部分 #3391(P1)。

🤖 Generated with Claude Code

https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH


Generated by Claude Code

claude added 3 commits July 25, 2026 04:32
…PR-1)
Introduce the spec's one source of truth for turning an object's
`enable.apiMethods` whitelist into its effective operation set, and make the
runtime dispatcher gate consume it.
- spec: new `@objectstack/spec/data` `api-derivation` module —
`resolveEffectiveApiMethods` / `isApiOperationAllowed` /
`effectiveOperationsArray` / `API_METHOD_DERIVATION` /
`DATA_ACTION_TO_API_OPERATION`. Three-state semantics (undefined =
unrestricted, [] = deny-all, subset = derived closure); the legacy 8 verbs
derive from the six primitives (get/list/create/update/delete/bulk).
restore/purge never derive (enable.trash retired, #2377). liveness note
updated.
- runtime: rewrite `api-exposure.ts` to consume the shared table. Fixes the
silent dead dispatcher/MCP gate — `getObject()` returns the flags nested
under `.enable`, which the flat-only reader ignored (nested-first, flat-
compatible). Flips `[]` from fail-open to deny-all.
- objectql: registration-time deprecation warning for standalone legacy
apiMethods values (`warnDeprecatedExplicitApiMethods`); cross-reference
comments distinguishing the verb→affordance (UI-intent) axis from the
verb→primitive (API-tightening) axis.
- plugin-security: cross-reference comment on the parallel WRITE_OP_AFFORDANCE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
…PR-2)
Route every per-object exposure gate through the spec's single derivation source
of truth, so the three-state whitelist and the derived verbs are resolved
identically everywhere.
- Main gate (`apiAccessDenialFromEnable`): resolver-backed; `[]` → deny-all;
the 405 body's `allowed` array is now the EFFECTIVE operation set (enum-
ordered), not the raw whitelist. `enforceApiAccess` forwards writeMode /
bulkChild opts.
- Import: two-stage gate — a coarse `create ∨ update` check 405s fully-closed
objects before the CSV parse; a writeMode-precise second stage
(insert→create, update→update, upsert→create∧update) after prep resolves the
mode.
- Export: reverse-derives from `list`, and the schema-derived column header is
projected to the FLS-readable set (union of masked-row keys) so export can
never expose a wider column set than list. Explicit `?fields=` is honored but
masked values stay empty.
- Bulk: all five surfaces (createMany/updateMany/deleteMany, per-object /batch,
cross-object /batch) require `bulk ∧ child`.
- platform-objects: the four in-repo explicit-whitelist identity objects gain
the `bulk` primitive so their Many/batch surfaces keep working.
Tests: rest exposure suite (405 allowed = effective, deny-all, bulk∧child),
new rest-api-derivation-gates suite, batch endpoint bulk cases, and two
blocking export FLS column-projection tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
#3391 PR-3)
Add the response-side contract that lets the server hand the frontend the
resolved effective operation set — the single channel the UI consumes, never the
raw whitelist.
- spec: `EffectiveObjectPermissionSchema` extends `ObjectPermissionSchema` with
an optional `apiOperations` array; `GetEffectivePermissionsResponse.objects`
uses it. The authoring `ObjectPermissionSchema` is deliberately NOT extended,
so a permission-set author can never declare a meaningless key.
- hono: `/me/permissions` now annotates each per-object entry with its effective
`apiOperations` (`annotateEffectiveApiOperations`), and for a modify-all
super-user seeds false-init entries for restricting objects absent from the
merged map (`seedSuperUserRestrictedObjects`) so fold pulls them true.
Sequence: seed → fold → clamp → annotate, each guarded (failure omits
apiOperations and the client falls back to default-allow).
- client: zero code — the typed surface carries the new field via the spec
re-export.
- changeset documenting the contract and the five behavior changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
@vercel

vercelBot commented Jul 25, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 25, 2026 4:46am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation protocol:data tests tooling size/xl labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/runtime, @objectstack/spec.

116 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/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime, 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 @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime, @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 packages/objectql, @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/cli.mdx(via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.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/your-first-project.mdx(via @objectstack/plugin-hono-server, @objectstack/runtime, @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/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/access-recipes.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql, @objectstack/plugin-hono-server, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via @objectstack/plugin-security, packages/runtime, @objectstack/spec)
  • content/docs/permissions/explain.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-security, @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/plugin-security, @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/objectql, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql, @objectstack/platform-objects, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/runtime, @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/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx(via packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/objectql, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime, @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 @objectstack/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/objectql, @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/objectql, @objectstack/plugin-hono-server, @objectstack/plugin-security, @objectstack/rest, @objectstack/runtime, @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/v16.mdx(via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/objectql, @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/audience-based-interfaces.mdx(via packages/plugins/plugin-security)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/plugin-security, @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/platform-objects, @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.

…3391)
Auto-generated reference doc for the new EffectiveObjectPermissionSchema
(`pnpm gen:schema && gen:docs`). Keeps content/docs/references in sync with
packages/spec — fixes the check:docs CI gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
Regenerate the public-API surface snapshot (`gen:api-surface`) for the 19 new
`@objectstack/spec` exports added by #3391 (the api-derivation module +
EffectiveObjectPermission). 0 breaking, 19 added — fixes the check:api-surface
CI gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
@os-zhuang
os-zhuang marked this pull request as ready for review July 27, 2026 01:21
@os-zhuang
os-zhuang merged commit ad4af62 into mainJul 27, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/complete-scheduled-development-xqcwch branch July 27, 2026 01:21
os-zhuang added a commit that referenced this pull request Jul 27, 2026
…n projection (#3547) (#3561)
The REST export route projected columns by inferring readability from the first
chunk of already-masked data rows (#3498), which drops an all-null readable
column and leaves an empty result set un-narrowed. Add the long-term-correct
path: a security-service query surface that returns the readable field set for
a context, derived from schema + FLS rather than from data rows.
- plugin-security: the `security` service gains getReadableFields(object,
context). Same evaluator + requiredPermissions fold + on-behalf-of delegator
intersection the read middleware's FieldMasker uses (fail-closed on a dangling
delegator), returning every schema field NOT masked non-readable — the exact
complement of maskResults' delete set, so it can never drift from data-plane
FLS. Computed from schema+context, never rows → immune to null/empty results.
isSystem bypasses FLS; unresolvable schema → undefined (caller falls back).
- rest: GET /data/:object/export asks the environment's security service for
getReadableFields and projects the schema-derived header to that set before
streaming. No service reachable → degrades to the existing masked-row
inference (zero regression). Explicit ?fields= is honored verbatim.
Tests: plugin-security getReadableFields (readable=complement of mask, isSystem
bypass, no-restriction passthrough, unresolvable→undefined, schema-not-rows);
rest export route projects from the service (drops a masked field present in
rows; keeps a readable column absent from every row; honors explicit ?fields=).
Claude-Session: https://claude.ai/code/session_012L8EfEa157Pe6C73qRnaJH
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude