Skip to content

fix(rest): 服务端闸门也过滤 areas[].navigation —— area 内导航项权限不再只是渲染层的礼貌 (#4722) - #4751

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-4722-filter-app-areas-nav
Aug 3, 2026
Merged

fix(rest): 服务端闸门也过滤 areas[].navigation —— area 内导航项权限不再只是渲染层的礼貌 (#4722)#4751
os-zhuang merged 2 commits into
mainfrom
claude/issue-4722-filter-app-areas-nav

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #4722

问题

filterAppForUser(packages/rest/src/rest-server.ts)是 app 元数据的服务端权威可见性闸门,但它只走顶层 navigation 树 —— if (!nav) return item 提前返回,item.areas 从头到尾没被读过。于是写在 area 内部导航项上的 requiredPermissions / requiresService 只有客户端 NavigationRenderer 会执行:该条目连同它的 objectName / pageName / componentRef 指向,照常出现在 /meta 响应体里。改一次前端状态、或者直接 curl 一下 /meta 的 JSON,就能看到本该被 gate 掉的条目。对 areas 型 app 而言,导航项级闸门此前不是服务端强制。

改法

同一个 filterNav 复用到每一棵 areas[].navigation 上 —— 不是第二份实现,所以两棵树对同一个键的语义不可能漂移(这是裁定第 2 问的要求)。列表 GET /meta/:type 与单项 GET /meta/:type/:name 两条路径都覆盖:两者都经过这个函数,且单项读对 app 类型本就绕过共享缓存,正是为了让这个 per-user 过滤能跑。

同一提交修正了 resolveRegisteredServices 的探测面。 它此前每个节点只取第一个命中的子数组(navigation / children / widgets 三选一),不下钻 areas。若不改,只在 area 内被引用的服务名根本不会被探测,而未探测的名字在闸门看来等同于「服务不存在」 —— 会把一个服务确实注册着的条目误剥离。探测面必须与过滤面完全一致,这不是顺手改,是本改动的正确性前提(有专门的 pin)。

三问的落实

1. 空 area 返不返回 —— 实测到的顶层既有行为,照抄。

顶层 filterNav 的分组处理(rest-server.ts 第 1836-1854 行,本 PR 未改动这段逻辑,只是复用):只有当 e.children非空数组时才进入过滤分支;过滤后,若该条目 type 为 group 且孩子被滤光,continue 掉它 —— 即剥离;否则带着过滤后的 children 保留。而 children 缺失或本来就是空数组的条目走 else 分支,原样保留

所以既有形状是:被闸门滤空的分组剥离,作者本来就写成空的分组保留。area 照此同形 —— 被 gate 滤空的 area 整个剥离(空壳标签没有消费价值),作者写成 navigation: [] 的 area 原样返回。过滤只负责报告调用方看不到什么,不负责整理元数据。这条区分有独立 pin(an area emptied BY the gate is dropped; an area authored empty is passed through)。

2. 两棵树一致 —— filterNav 复用,零复制。area 内的嵌套 group 折叠行为也自动与顶层一致(有 pin 对照)。

3. visible CEL 明确不做 —— 服务端求值需要绑定 user 上下文,不是这个读路径现有的能力。这个不对称写进了三处:filterAppForUser 的文档注释、packages/spec/liveness/app.json 的账本 note、以及 rest.test.ts 里一条新的 characterisation pin(钉住「服务端在任何层级都不求值 visible」,并写明谁把 CEL 搬到服务端就该看到它红)。requiresObject 同理仍是客户端。

退役键未复活:#4651 移除的 area 级 visible / requiredPermissions 一个都没回来。本次强制的是 area 里面的项级闸门 —— 这正是 #4651 留下的真缺口。

已预期的钉,已按预案改写

rest.test.tscharacterises the boundary the #4651 prescription warns about: areas is not walked 那条 —— 它的注释写着「谁让服务端开始走 areas 就该看到它红,然后连同账本 note 一起有意识地改写」。这就是那次改写:重写成 AREA level: the item gate applies inside areas[] too,并同步改写 packages/spec/liveness/app.jsonareas.navigation note(以及顶部 _note 里同一句已经变假的陈述、navigation.requiredPermissionsnavigation.visible 两条 note)。

⚠️ spec 零改动约束:只动了 packages/spec/liveness/app.json 这一个文件的账本 note —— 派发时开的单文件例外。packages/spec/** 其余一行未改。

留了一个尾巴,已单独立单

packages/spec/src/ui/app.zod.ts:662AREA_REQUIRED_PERMISSIONS_RETIRED 处方正文仍写着「the server does not walk areas」,packages/spec/src/ui/app.test.ts:1374 还有一条断言把这个措辞钉死。改它是处方正文 + pin + spec 生成物的耦合改动,撞上本单的 spec 零改动约束,故按 Prime Directive #10 记为 #4749(未认领)。方向上它是「过度保守」而非不安全 —— 劝作者把敏感项挪到顶层树,这仍然可行 —— 但它是错的,而且是说给作者听的那一份。

验证

pnpm --filter @objectstack/rest test
  Test Files  38 passed (38)
  Tests  571 passed (571)

新增/改写的测试先证明它们能红:把 rest-server.ts 的改动 stash 掉后重跑,

  • rest.test.ts:Tests 6 failed | 191 passed —— 4 条 AREA level + 2 条 [#4722] requiresService/探测面
  • meta-app-area-nav-gate.test.ts(新增,路由级):Tests 3 failed | 2 passed

路由级那一份是验收标准的直接证据 —— 断言的是响应体字节:无权限调用方拿到的 /meta 列表与单项 JSON 里,secret_forecast / secret_ops_page / secret_sys_user 这些 gate 掉条目的指向一个都搜不到,同时有权限的调用方拿到完整树(不过度过滤)。

其他:pnpm --filter @objectstack/spec check:liveness ✓(账本改动通过,evidence 行号已同步);eslint --no-inline-config 三个文件干净;tsc --noEmit -p packages/rest 错误数 67,与 origin/main 基线逐一相同(该包在 #4311 的 DEBT 账本里,无 typecheck 脚本);node scripts/check-type-check-coverage.mjs OK。已合入最新 origin/main(#4742)后重装、重建、重跑,增量提交不与 packages/rest / packages/spec 重叠。

消费方注意(changeset 已写清)

服务端响应形状收紧:无权限用户拿到的 app 元数据里,被 gate 的 area 内导航项不再出现,被滤空的 area 整个不见。任何依赖「服务端全量下发、客户端自己藏」的消费方需要改为信任服务端已过滤的树 —— 这正是本次收紧的目的。

🤖 Generated with Claude Code

https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny

claude added 2 commits August 3, 2026 02:10
…level tree (#4722)

`filterAppForUser` is the server-side authority for app metadata visibility,
but it walked only the app's top-level `navigation` tree: it returned early
when `navigation` was absent and never read `item.areas` at all. An item gated
inside an area was therefore enforced by the shell alone — the entry, and the
`objectName` / `pageName` / `componentRef` it points at, still shipped in the
`/meta` body, so reading the JSON (or poking client state) defeated the gate.

The same `filterNav` now runs over every `areas[].navigation`. Reusing it is
the point: one implementation means the two trees cannot drift on what
`requiredPermissions` / `requiresService` mean. Both read paths are covered
because both go through this function — the list `GET /meta/:type` and the
single-item `GET /meta/:type/:name` (which already bypasses the shared cache
for apps so this per-user filter can run).

Collapse rule, taken from what the top-level tree already does to a `group`:
an area emptied BY the gate is dropped (a bare label with nothing reachable
under it has no consumer value), while an area authored empty is passed
through — filtering reports what the caller may not see, it does not tidy the
metadata.

`resolveRegisteredServices` had to learn the same walk. It took one child list
per node (`navigation` / `children` / `widgets`, first match wins) and never
descended into `areas`, so a service named only inside an area went unprobed —
and an unprobed name is absent from `registered`, which the gate reads as
"service missing" and would strip a live entry. The probe set now covers
exactly what the filter walks.

Deliberately unchanged: `visible` (CEL) stays a client-side gate at every
level — evaluating it server-side needs a bound `user` context this read layer
does not have, and is its own change. The asymmetry is recorded in the code
comment, in the liveness ledger, and in a characterisation pin. The retired
AREA-level keys (#4651) are not revived; what is enforced is the item gate
inside an area.

The `areas is not walked` characterisation pin fails as its own comment
predicted and is rewritten here, together with the `areas.navigation` note in
`packages/spec/liveness/app.json` that asserted the same thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Br2xsJsczFsTR9bvbh2Ny
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 2:17am

Request Review

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

109 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/connect-mcp.mdx (via @objectstack/rest)
  • 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/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 @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/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/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • 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/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/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • 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 packages/rest, @objectstack/spec)
  • content/docs/protocol/kernel/index.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 @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/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/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/rest, @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.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 3, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 3, 2026 02:58
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 3, 2026
Merged via the queue into main with commit 05d8a54 Aug 3, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4722-filter-app-areas-nav branch August 3, 2026 03:10
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

2 participants