Skip to content

Commit 40cc88e

Browse files
committed
Merge origin/main into claude/issue-15150-monorepo-only-escaping-path
2 parents 3c53ed9 + f7a9740 commit 40cc88e

31 files changed

Lines changed: 2080 additions & 109 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
---
4+
5+
fix(plugin-auth): one base-path normalisation chain, and an MCP resource identifier that is always a URL
6+
7+
`AuthManager` derived its base path in three independent places. `getMcpResourceUrl()`
8+
read `this.config.basePath` directly and added no leading slash, so a `basePath`
9+
configured without one produced a value that is not a URL at all:
10+
11+
basePath 'api/v1/auth' -> http://localhost:3000api/v1/mcp
12+
13+
`new URL()` throws on that (`3000api` is not a port), so the RFC 9728 path-inserted
14+
well-known route derived from it throws too, and `@better-auth/oauth-provider` 1.7.2
15+
refuses to seed the `sys_oauth_resource` row from it at plugin init ("resource
16+
identifier ... must be an absolute URI (RFC 8707 §2)"). With
17+
`enforcePerClientResources` at its `true` default, every MCP client was then refused
18+
for want of a link row. That input class could never mint or match a token, so
19+
repairing it re-selects nothing.
20+
21+
There is now exactly one read of the configured value and one chain above it:
22+
23+
configuredBasePath() the configured value VERBATIM — what better-auth is handed
24+
└─ rootedBasePath() + a leading slash when absent (better-auth's own rule)
25+
├─ getAuthIssuer() = origin + this
26+
└─ getBasePath() = this, trailing slashes stripped
27+
└─ getMcpResourceUrl() = origin + this minus `/auth` + `/mcp`
28+
29+
`getAuthIssuer()` and `getBasePath()` answer byte-identically to before for every
30+
spelling. Only `getMcpResourceUrl()` moves, and only for a non-canonical `basePath`:
31+
a missing leading slash (was not a URL), repeated trailing slashes, or a configured
32+
`/` (was a `//mcp` path no mount serves). A canonical `basePath` is unchanged on all
33+
three getters.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
The lookup-picker "who reads this" claims in `packages/spec` are re-measured against objectui and dated to the commit they were measured on. No schema, accept set, default or refusal moves — this is evidence prose, and every verdict it sits under is unchanged.
6+
7+
Three claims had gone false, all in the same direction: they credited objectui's picker with reading a `snake_case` alias that objectui no longer reads. A stale *tolerance* claim fails in the dangerous direction — it tells an author a spelling is accepted downstream when it is not, so a value that will silently arrive as nothing looks supported by the spec's own prose.
8+
9+
- **`liveness/field.json`, both `displayField` notes.** `/props/displayField` claimed the record picker "reads displayField || display_field"; `/props/inlineColumns/children/displayField` named the `snake_case` spelling flatly as *the* key the grid's lookup cells pass. objectui deleted that twin from `LookupFieldMetadata` with no deprecation window and no dual read. Both notes now name the read chain they actually have — `LookupField.tsx`'s `fieldMeta?.displayField || fieldMeta?.reference_field || 'name'`, and `GridField.tsx` handing the column's camelCase `displayField` straight through at all three lookup-cell call sites. Both entries stay `status: "live"`: `displayField` is live, and more exclusively so than the notes claimed.
10+
- **`src/data/field.zod.ts`, the LOOKUP PICKER (forward) docblock.** It told authors that objectui's `LookupField` / `RecordPickerDialog` / `deriveLookupColumns` read "both these camelCase keys and their snake_case aliases" — a blanket claim over all seven keys declared beneath it. Measured, it holds for three: `lookupColumns`, `lookupPageSize` and `allowCreate` are each read as `<snake> ?? <camel>`. The other four — `displayField`, `descriptionField`, `lookupFilters` and `dependsOn` — are read camelCase-only. The docblock now states that per key, keeps saying the truth for the three aliases that survive, and records that those three are objectui's own back-compat rather than a spelling this schema declares.
11+
- **`liveness/field.json`, the `valueDomain` `evidence` string.** It described the shared membership predicate as one "the write path **will** call" while its own first clause already quotes the landed call site that calls it. Tense corrected; the pointer is unchanged.
12+
13+
Each rewritten claim now names the objectui commit it is dated to, so a later reader can tell how old the evidence is instead of assuming it is current. That dating is prose by design: a gate over a pinned foreign tree would go stale at every pin bump and need its own anti-vacuity self-test, which is a worse trade than a dated sentence.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/plugin-audit': patch
3+
---
4+
5+
fix(plugin-audit): record-view rows keep the VIEW instant instead of the buffer-drain instant (#16829)
6+
7+
`sys_audit_log`'s `record_views` rows answer "when did this user look at this record?". Read auditing batches its INSERTs off the request path by design, so `buildRow` writes `created_at: event.viewedAt` rather than letting the column's `NOW()` default stamp a whole batch with one flush timestamp — up to `flushIntervalMs` after the fact, with read order inside the window destroyed.
8+
9+
`persistReadAuditRows` wrote that row under `{ context: { isSystem: true } }`, and the module's comment cited that flag as what carried the view instant through. It never was. `isSystem` exempts a write from the readonly strip; the layer that decides `created_at` on an insert is the audit stamp hook `sys_stamp_audit_insert`, which reads `session.preserveAudit` and has never read `isSystem`. What was actually carrying the value was that hook's pre-#15964 line, `record.created_at = record.created_at ?? now` — client-preferred on every insert, with no flag and no privilege required. #15964 closed that accident (maintainer ruling 2026-09-06), and the ordinary branch has stamped `now` since: on this path, the flush instant.
10+
11+
The write now declares both context keys, for two different layers:
12+
13+
```ts
14+
await engine.insert(
15+
'sys_audit_log',
16+
rows as any,
17+
{ context: { isSystem: true, preserveAudit: true } } as any,
18+
);
19+
```
20+
21+
`isSystem` still carries the readonly-strip exemption the row needs; `preserveAudit` is the one the stamp hook reads. `preserveAudit` is the ruled historical-import channel (#3493, reaffirmed by #15964's ruling) — the door audit left open for reinstating an original timeline — and a view row's original timeline is the moment of the view, so this use is inside its declared purpose rather than a bypass of it.
22+
23+
**What changes for a deployment.** Only for deployments that opted objects in to record-view auditing (`AuditPlugin`'s `readAudit.objects`). Rows written from now on carry the view instant. ⛔ Rows already written under the flattened behaviour are not repaired by this change: their `created_at` is the drain time of the batch they were in, and the view instant they should have carried was never persisted anywhere else, so it cannot be recovered. Only builds cut from `main` after #15964 are affected — the objectql half has not shipped in a published version.
24+
25+
**No exported symbol, schema, route or config key moves.** The only observable change is that a `created_at` this writer already intended to write now survives.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
New gating rule `security-fls-unknown-field`: an object-qualified field-permission key naming a field the object does not declare is now an authoring-time `error`.
6+
7+
`security-fls-unqualified-key` has always caught the *bare* spelling — `fields: { budget: … }` — because the runtime evaluator matches FLS keys by their `<object>.` prefix and a bare key matches nothing. The qualified-but-dangling spelling (`fields: { 'crm_account.description_nope': { readable: false } }`) has the identical runtime consequence and was reported by nothing: `PermissionEvaluator.getFieldPermissions` strips the prefix and looks the remainder up as a column, so a remainder no column answers to contributes nothing to the merged permission map. The masking the author declared **never enforces**, and the field stays as readable and as editable as the object-level grant leaves it — for every holder of the set.
8+
9+
The failure direction is **fail open**, and this spelling is the one that accumulates: unlike a bare key it looks correct in review, survives rename refactors invisibly, and is exactly what a field rename leaves behind.
10+
11+
- **A second rule, not a widening of the first.** `security-fls-unqualified-key` is correct inside its declared scope and is untouched; the two defects have different prescriptions (add the object prefix / fix the field name) and suppressing one must not suppress the other. Two ids, two messages.
12+
- **Where the existence answer comes from.** The rule resolves through `object-graph.ts`, the shared index every field-existence rule in this package already uses — no new input path. It therefore inherits that module's three skips, each of which is the difference between a finding and a false one: an object this stack does not define (it may be another installed package's), an object with no readable field map (an ADR-0015 `external` object, an introspected datasource), and registry-injected system columns such as `created_at` or `owner_id`, which are real at runtime and appear in no authored `fields`.
13+
- **A truncated key is the same defect and is reported by the same rule.** `fields: { 'crm_account.': … }` passes the runtime's prefix test and resolves to the empty column name, so it matches nothing exactly as a dangling name does. `PermissionSetSchema.fields` is `z.record(z.string(), FieldPermissionSchema)` — a bare string key with no pattern and no refinement — and this rule is the only reader of those keys, so before this change nothing reported it at all. A key naming an object this stack does not declare still falls to skip 1, truncated or not.
14+
- **It mirrors the evaluator, including on a multi-dot key.** Only the first dot separates object from field, because `ObjectSchema.name` is `/^[a-z_][a-z0-9_]*$/` and cannot contain one. `'crm_account.owner.name'` therefore asks for a column literally named `owner.name` and is reported: FLS keys address columns, never joins, and resolving that as a relationship hop would have been a fail-open divergence from the gate the rule mirrors.
15+
16+
**What moves for consumers.** A stack carrying a dangling FLS key built clean before and now fails `os validate` / `os compile`, and is refused at the runtime publish door for `permission` and `object` writes (this rule joins the existing `validateSecurityPosture` registration; no new registry entry). That is the point — the key was never enforcing anything. A stack whose FLS keys all resolve is byte-identically clean: measured on the shipped showcase, whose six authored keys emit zero findings, with a firing control (one injected dangling key produces exactly one finding) beside the zero.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
"@objectstack/client": patch
4+
---
5+
6+
`POST /two-factor/verify-totp` and `/two-factor/verify-otp` now echo the user row as it stands when the response is written, instead of the pre-rotation snapshot the vendor closes over.
7+
8+
On the enrolment lane — a signed-in caller confirming a new factor — better-auth writes `twoFactorEnabled: true`, rotates the session, and only then calls the `valid(ctx)` closure it built at entry. That closure still holds the pre-rotation session, so a successful verification answered `user.twoFactorEnabled: false` to the very caller who had just switched 2FA on. An account portal reading that body renders the factor as still OFF right after enrolment, and a bearer client that caches the echoed user carries the wrong flag until its next `get-session`.
9+
10+
`two-factor-rotated-token-echo` already repaired the body's other stale member, `token`, on exactly these routes and on exactly this predicate — the response staged a session cookie whose token differs from the one echoed. The `user` member is stale for the same reason, so it is repaired under the same predicate rather than a new one.
11+
12+
- **Two narrowings, both load-bearing.** Only the members the vendor already echoed are written, so the published payload shape (`AuthWireUser`) cannot widen — better-auth's own output filter is a deny-list, and forwarding a raw row would put every column it happens to carry on the wire. And the row is re-read through `internalAdapter` by the id the response itself published, so the repair travels the same output transform that produced the echo (a driver that stores booleans as `1`/`0` cannot change a member's wire type) and can never substitute a different principal into a response.
13+
- **`/two-factor/verify-backup-code` is untouched.** It does not rotate and already echoed the live row; it is in neither path list, its row is not read, and it is pinned as a negative control on both the in-memory engine and a real `SqlDriver` — an unconditional re-read would have "fixed" the broken lane and quietly rewritten one that was already right.
14+
- **The failure posture is inherited.** A row read that throws or answers nothing degrades to the vendor's own echo, never to a failed verification and never to a lost `token` repair, which is written first for that reason.
15+
16+
`@objectstack/client` drops the `AuthTwoFactorVerificationResult.user` warning that told callers to re-read the session for the live flag; the wire shape it declares is unchanged.

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,18 +511,17 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
511511
- 仅 `references/**` 面、或治理面上一行级机械文本改动的卡,降为默认判断档施工。
512512
- 纯机械一行 PM 酌定可至下限档;降档施工的补偿控制 = 复核席跑契约复审档。
513513
- 降档施工只经契约复审档复核到达维护者,⛔ 不新增标签不新增链。
514-
- 强制条款②:凡放宽接受集或扩大公开面的卡一律契约复审档;拉回已声明契约按常规档。
514+
- 强制条款②:凡放宽接受集或扩大公开面的卡默认判断档施工、契约复审档复核。
515515
- 条款②判据即代裁的机械边界测试与 `references/lanes/spec.md` 席内分派判据,⛔ 不另抄。
516516
- 负边界:运行时权限/安全行为变更不是条款②,归人工地板安全/权限边界类。
517-
- 条款②只指已发布契约面。
517+
- 条款②只指已发布契约面,拉回已声明契约不触它;卡面复述仍是条款②。
518518
- 删已发布契约文本本就否定的误拒本身是 `no`;该 `no` 须引那段文本,缺引即读作缺申报。
519519
- 引文不符 ⇒ 降为 `yes`;裁决守卫、安全拒绝、失败关闭是刻意拒绝非误拒,删之 `yes`。
520520
- 错误码已发布面 = `ERROR_CODE_LEDGER`/`StandardErrorCode`:新码恒 `yes`,无门亦然,漏登即缺口。
521521
- 机械放宽判别门禁落地前,入队前的席内契约复核是方向自述的补偿控制。
522-
- 卡面对条款②的复述仍是条款②;额度耗尽豁免及其 `needs:contract-review` 补偿一并及于它。
523522
- 豁免够不到的地板只有维护者裁决能设。
524523
- 席位档策略:skills 车道外的执行席与分诊席默认判断档会话。
525-
- 契约复审档留给裁决/复审子代理、skills 席与条款②工作。
524+
- 契约复审档留给裁决/复审子代理、skills 席与条款②复核。
526525
- 降档出口两条:额度耗尽豁免,与主动预降(余量吃紧可预先降档)。
527526
- 额度耗尽豁免仅当契约复审档实测不可用才落默认判断档,⛔ 不再往下。
528527
- 降档的档位与理由记入认领评论 Container & model 行;档位逐次派发显式传参,永不省略。
@@ -637,7 +636,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
637636

638637
- 细则见 `references/landing-operations.md`,落地窗口查阅。
639638
- 条款②入队闸门:翻 ready / 入队前先取 PR 实际 diff;diff 是事实,卡片语义是预测。
640-
- `--tier` 嫌疑行是提示非裁定;双肢命中任一且派发档位低于契约复审档 ⇒ ⛔ 禁止入队。
639+
- `--tier` 嫌疑行是提示非裁定;双肢命中任一 ⇒ 无席内契约复审档 PASS 在案 ⛔ 禁止入队。
641640
- 路径肢 = diff 触及契约面 `packages/spec/src/**`,含 error-code-ledger 与 `*.zod.ts` 契约 schema。
642641
- 声明肢 = 认领评论声明 `Clause-②: yes`,与路径无关;错误的 `no` 是可审计的假申报。
643642
- 交付后复核由派发席在席内完成:达档 PM 自审,或派契约复审档复核子任务。
@@ -673,6 +672,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
673672

674673
- 每轮向维护者打中文轮次报告(chat 通道):issue → 判决 → PR 链接 → 备注的表。
675674
- 报告加升级项、代裁清单(分诊)、awaiting a human merge 项、governed 合并审计清单。
675+
- 报告席记条款②默认档 FAIL 率入复审清单;超改制前达档史值 ⇒ 决策卡交维护者定回退。
676676
- 审计清单实跑 `node scripts/pm/check-governed-merges.mjs --since <上轮>`,⛔ 不凭记忆汇总。
677677
- 车道审计是早警;权威合并窗口与认定/回滚处置归总监席,见 `references/lanes/director.md`。
678678
- 报告含 `UNRECOGNISED` 行:对本轮门禁日志 grep `UNRECOGNISED` 逐行照录,`NOT APPLICABLE` 行也在内。

‎.claude/skills/pm-dispatch/references/contract-review.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
- 内容肢及于 published `skills/**`:作可证伪的算子或契约语义主张的改动挂标走本复核。
88
- ⛔ 判据不是提到契约:纯算子清单、拼写、格式不触发。
99
- 受管面人工合并 ⛔ 不替代复核,两道保障叠加。
10-
- `Clause-②: yes | no` 按设计临时:只定派发档位的保守方向,⛔ 非终审。
10+
- `Clause-②: yes | no` 按设计临时:只定是否必过契约复审档复核的保守方向,⛔ 非终审。
1111
- 删误拒的 `Clause-②: no` 附 `Contract-text:` 引已发布契约文本,双载体同载;缺引即缺申报。
1212
- 真闸门在 PR 或报告时点的档位复核;声明被档位推翻 ⛔ 不作席位过失。
13-
- 机械地板 claim 时可查树:新导出符号或已发布载荷上的新键恒 `yes`,直接锁契约复审档。
13+
- 机械地板 claim 时可查树:新导出符号或已发布载荷上的新键恒 `yes`,锁契约复审档复核。
1414
- conformance 类 ⛔ 不机械化:填充已声明字段、在两个已发布码之间重选输入类都需判断。
15-
- claim 拿不准 ⇒ 按 `yes` 派契约复审档;⛔ 不建全量分类学与 claim 时决策程序。
15+
- claim 拿不准 ⇒ 按 `yes` 挂标走契约复审档复核;⛔ 不建全量分类学与 claim 时决策程序。
1616
- PR 与卡双载体同笔挂:PR 一存在即挂;报告先于 PR 到达则先挂卡侧,ACCEPT 时补齐 PR 侧。
1717
- 同笔在卡上记一行条款②认定;停靠只防误入队,⛔ 无等外部席位的无限期状态。
1818
- 前瞻的条款②事实住卡上裁决与分诊评论、`Clause-②` 申报和 `--tier` 输出。

0 commit comments

Comments
 (0)