feat(spec): publish ISecurityService and enforce it at both ends#3660
Merged
Conversation
The `security` service registers seven cross-package methods but had no contract in `@objectstack/spec/contracts`. Consumers duck-typed it and each invented its own fallback for a missing method or an "empty" answer. Adds `ISecurityService` and types BOTH ends against it, so the surface is enforced rather than declared: plugin-security assigns its registration to the interface (a renamed, dropped, or re-typed method fails that build), and the REST layer resolves the service as `Partial<ISecurityService>` (call sites must keep feature-detecting rather than assume the full surface). The contract states the one thing consumers cannot guess — the methods do not share a failure convention. `getReadFilter` fails CLOSED (a failure is a deny filter, never `undefined`; `undefined` means "no row restriction" and nothing else). `getReadableFields` fails SOFT, and its two empty answers are opposites: `undefined` is "no answer, use your own projection", `[]` is authoritative "no field is readable". Typing the producer immediately caught one real discrepancy, fixed here: getReadFilter declared `Record<string, unknown> | null | undefined` while every return path yields a filter or `undefined` (`filter ?? undefined` normalizes the null away). Dropping the dead `| null` leaves "no restriction" with exactly one representation. Type-level only. Tests: spec 6665, plugin-security 593, rest 399 — all passing; the three packages build clean (CJS/ESM/DTS). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The contracts index table and the runtime-services source-of-truth list enumerate every published service contract; a new contract missing from them makes the index quietly wrong. Adds the Security Service row and its source path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
Adding ISecurityService and its companion types exports six new names from `@objectstack/spec/contracts`; the api-surface gate pins the public surface so additions have to be acknowledged. Diff is exactly those six additions — 0 removed, 0 narrowed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
os-zhuang
marked this pull request as ready for review
July 27, 2026 13:35
os-zhuang
pushed a commit
that referenced
this pull request
Jul 27, 2026
…rix is not a failed one Closes #3668. With cancel-in-progress on, every consecutive push cancelled the in-flight dogfood matrix (the longest job in the workflow) and the gate's catch-all branch turned that into a red X on the superseded SHA — two observed on #3660 alone. False reds train everyone to ignore the one check that must never be ignored. Safety premise verified experimentally before landing (per the issue's own ask): run 30271824408 executed a fail-fast matrix where shard 1 really failed and fail-fast cancelled shard 2 mid-run — the aggregate needs.<job>.result reads 'failure', not 'cancelled'. Failure dominates, so an aggregate of 'cancelled' can only come from the whole run being stopped externally (supersession or manual cancel) and passing it masks no real regression. The manual-cancel case going green is the issue's accepted trade-off; the rejected alternative (skipping the gate via !cancelled()) would republish the #3622 required-context deadlock. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECTCrcCdZpCHw5zFSgcmGt
os-zhuang
added a commit
that referenced
this pull request
Jul 27, 2026
…rix is not a failed one (#3671) Closes #3668. With cancel-in-progress on, every consecutive push cancelled the in-flight dogfood matrix (the longest job in the workflow) and the gate's catch-all branch turned that into a red X on the superseded SHA — two observed on #3660 alone. False reds train everyone to ignore the one check that must never be ignored. Safety premise verified experimentally before landing (per the issue's own ask): run 30271824408 executed a fail-fast matrix where shard 1 really failed and fail-fast cancelled shard 2 mid-run — the aggregate needs.<job>.result reads 'failure', not 'cancelled'. Failure dominates, so an aggregate of 'cancelled' can only come from the whole run being stopped externally (supersession or manual cancel) and passing it masks no real regression. The manual-cancel case going green is the issue's accepted trade-off; the rejected alternative (skipping the gate via !cancelled()) would republish the #3622 required-context deadlock. Claude-Session: https://claude.ai/code/session_01ECTCrcCdZpCHw5zFSgcmGt Co-authored-by: Claude <noreply@anthropic.com>
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.
背景
#3547 收尾时的观察:
security服务注册了 7 个跨包方法(getReadFilter、getReadableFields、resolvePermissionSetNames、explain,以及三个 audience-binding suggestion 调用),但@objectstack/spec/contracts/下没有它的契约——而那个目录里已经躺着 40+ 个方法型服务契约(ISharingService、IEmailService、IAnalyticsService…)。消费者只能鸭子类型地探测它,每个消费者各自发明「方法缺失怎么办」和「空答案是什么意思」的回退策略。#3547 之前只有一两个消费者,现在开始变多,这就是一个漂移面。
改动
新增
packages/spec/src/contracts/security-service.ts,并把两端都对着它做类型检查 —— 关键是enforced 而非 declared(Prime Directive #10):ISecurityService。方法改名、漏掉、返回类型变了,当场构建失败,而不是等消费者的特性探测在运行时静默降级。Partial<ISecurityService>,强制调用点继续做typeof x === 'function'探测,而不是假定完整表面。契约里写明了消费者猜不出来的那件事:这些方法不共享同一套失败约定。
undefined的含义getReadFilterundefinedgetReadableFields[]是权威答案,意思相反:一个字段都不可读这个区别是承重的:字段投影只是在「已经发生的强制」之上做外观收窄(读路径早已删除不可读键),退化到更宽的列集不会泄露值;行过滤器没有这层兜底。
类型检查当场咬出的一处真实不一致(已修)
getReadFilter声明返回Promise<Record<string, unknown> | null | undefined>,但每条 return 路径要么是过滤器对象要么是undefined——filter ?? undefined已显式把 null 归一掉了。| null是死分支,却让「无限制」有了两种表示,正是契约该拒绝的歧义。已删除。纯类型变更,无运行时行为改变。测试
security-service.test.ts(6 例):完整实现满足表面;getReadFilter的undefined只能表示「无限制」;getReadableFields的undefined与[]是相反答案;system context 全字段绕过;部分实现可被特性探测;explain接受 record 级请求与显式目标用户。关联
#3547 的 follow-up(服务面契约固化)。关联 #3391、#3561、#3649。
🤖 Generated with Claude Code
https://claude.ai/code/session_01KZ2BGusRo58ZW8FMkDhGTb
Generated by Claude Code