Repository navigation
Commit 5bb9a83
Fixes #23531
Clause-②: no
This lands the remainder of #23531: the `currentRecord` half that PR
#23582 held back. The hold waited on #23586, whose answer landed as PR
#23628 (`7dc7d2213`): the protocol now reads that row as the caller.
## What this does
A stale `If-Match` (or `expectedVersion`) on `PATCH` or `DELETE
/data/:object/:id` is refused by the protocol with
`ConcurrentUpdateError` (`409 CONCURRENT_UPDATE`). Both Hono doors
render it through `declaredEnvelopeForThrow`. They are
`@objectstack/hono`'s `createHonoApp` and a route mounted on
`HonoHttpServer`. That function reads the structured fields off
`RestServer`'s classification (`structuredCodeAnswer`) in
`declaredArmContext`. That helper dropped `currentRecord` by name,
because the record used to be the row the existence probe
(`probeRecord`) reads with a system context.
Since PR #23628, on a version mismatch `metadata-protocol` re-reads the
row under the caller's own context. A row the caller's row-level rules
hide answers `404 RECORD_NOT_FOUND` with neither field. A readable row's
`currentRecord` is the caller's field-masked read. So the hold is
lifted:
- `declaredArmContext`
(`packages/plugins/plugin-hono-server/src/adapter.ts`) now drops only
the arm's own `error` and `code`. `currentRecord` rides `error.details`
beside `currentVersion`, as `RestServer` forwards it flat.
- The docblock lines that said the field is held are rewritten. The
field is forwarded because the producer reads it as the caller. An arm
that grows a field read with a system context must still be held there
by name until its producer reads it as the caller.
- FROM
`{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…"}}}`
- TO
`{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…","currentRecord":{…}}}}`
No other arm's behaviour changes. `metadata-protocol`, `packages/types`
and `packages/adapters/hono/src/index.ts` are not edited. Changeset:
`@objectstack/plugin-hono-server` and `@objectstack/hono`, `patch`, as
PR #23582 did (one fixed version group, and `createHonoApp`'s answer is
the measured door).
## Pins
-
**`packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts`**
(direct-mount route): the `[#23531]` rows flip from absence to presence.
`error.details` must equal `{ currentVersion, currentRecord }` exactly,
as the producer stated them, so a field the door drops or invents turns
them red. The controls are kept: the stored version is accepted, and a
conflict stating no current state keeps the two-key envelope.
-
**`packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts`**
(a real `createHonoApp` over the real dispatcher and protocol, on SQLite
WASM):
- `PATCH` and `DELETE` with a stale `If-Match`: `error.details` equals
the `currentVersion` and `currentRecord` the real protocol threw, read
off the protocol call, and the record names the stored row. The
`currentVersion` echo pins are kept: echoed as the next `If-Match` it is
accepted (`PATCH` writes, `DELETE` deletes).
- New, `PATCH` and `DELETE`: a row whose `secret` field the caller's
read masks. `currentRecord` carries the row's `id` and `name` and no
`secret`, and no secret text is on the wire. The record equals both the
caller's own read and the record the protocol threw, while the store
still holds the field.
- The mask is an engine middleware that rewrites a caller-context read
after `next()`. That is the seam `@objectstack/plugin-security`'s
field-level rules rewrite a read at. The security plugin itself is not
booted there, because `http-conformance` does not depend on it, and
adding it means `package.json` and the lockfile, outside this claim. The
real field- and row-level rules are pinned by
`packages/runtime/src/domains/data-conflict-caller-visibility.test.ts`
(PR #23628), and are measured on this head below.
## Verification (at `4bc9b0c08b`)
The branch merged `origin/main` at `286eb9ef3` (4 commits, none touching
these files) before the PR opened. Every reading below is on
`4bc9b0c08b` unless it names another head.
- **Packages**, after a full `turbo run build` of the workspace (75/75):
- `pnpm --filter @objectstack/plugin-hono-server typecheck` (tsc,
`tsconfig.typecheck.json`, `check:test-typecheck` OK with 0 debt) and
`test`: Test Files 29 passed, Tests 354 passed.
- `pnpm --filter @objectstack/http-conformance typecheck`
(`check:test-typecheck` OK, ledger unchanged) and `vitest run`: Test
Files 13 passed, Tests 144 passed.
- Consumer pins in `packages/runtime`, which resolves
`@objectstack/plugin-hono-server` through `dist/`:
`data-conflict-caller-visibility`, `data-update-expected-version` and
`data-delete-expected-version` gave 3 files, 115 passed.
- **Ablation** (at `7a8e9bf386`): `node scripts/ablation-replace.mjs` in
wrap mode, plus an outer `trap` restore on EXIT INT TERM over the
absolute path. The exclusion was put back: anchor x1 to x0, replacement
carrying the marker `ABLATION_23531_RESTORE_HOLD` x0 to x1, blob
`4b7ca0f62478` to `3e3313e4060c`. `plugin-hono-server` was rebuilt, and
`ablation-dist-preflight` found the marker in `dist/index.js` and
`dist/index.mjs`. The direction was as predicted:
- unit file: 2 failed, 35 passed. PATCH and DELETE gave `expected {
Object (currentVersion) } to deeply equal { …(2) }`;
- conformance file: 4 failed, 16 passed. These are the two presence rows
and both masked-field rows (`expected undefined to match object`).
- Restore: the blob equals HEAD and `git diff HEAD` is empty. The
rebuild was 6/6 turbo cache hits. Preflight `--absent` found the marker
gone from all 6 built files, with a clean tree. Both files were green
again (37/37, 20/20).
- **Door parity with the real security layer** (at `7a8e9bf386`, a
scratch test in `packages/runtime`, deleted after the run and never
committed): the same stale `PATCH` and `DELETE` ran on the FLS-masked
row and on the plain row. The Hono exit's `error.details` equals
`RestServer`'s `{ currentVersion, currentRecord }`, and neither wire
carries the masked value: 4/4. The committed
`data-conflict-caller-visibility.test.ts` was green beside it (16/16).
- **Gates**: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (7 paths vs merge base `286eb9ef3`) derived
95 commands. All 95 ran, each exit captured before any pipe: 95 x exit
0. `--ran` reports 95 derived, 95 run, 0 NOT-MEASURED and 0 UNRUN.
- Pass 1 at `7a8e9bf386` caught `check:query-options-erasure`: two `as
any` on `findOne` options in the new pin took the test surface from 236
to 238. Fixed in `bc83077a4f`, back to 236.
- **Lint**, narrowed: `eslint --no-inline-config` on the 3 changed TS
sources exited 0, with 0 errors and 0 warnings each.
- Each file was really linted: 6/5/5 active rules by `--print-config`.
- `eslint.config.mjs` is unchanged from base to `origin/main`.
- The 3 `.mdx` pages fall outside ESLint's config (`File ignored`), as
they do under `pnpm lint`.
- The full `pnpm lint` is CI's (`Lint · ESLint`).
## Acceptance notes
- **Docs corrected, outside the claim's listed file surface.** Three
pages said a Hono door never sends `currentRecord`. PR #23616 wrote
them, and this change makes them false: `content/docs/api/index.mdx`,
`content/docs/api/wire-format.mdx` ("Hono hosts nest the extras", whose
example now shows `currentRecord`), and
`content/docs/protocol/kernel/error-handling.mdx` (the Hono-door table
and the "never sends" line). They are corrected here, with one line each
saying the record is the row as the caller reads it.
- **Two unreleased changesets still say `currentRecord` stays held:**
`.changeset/23531-hono-conflict-body.md` and
`.changeset/23587-hono-structured-arm-fields.md`. They are not edited
here (outside this claim). This PR's changeset says it supersedes that
note. If one CHANGELOG story is preferred, those two sentences can be
cut before the next release. Carrier: the seat.
- **The runtime pin's dispatcher-door rows** read `currentRecord` off
the thrown error, not the wire, because the wire did not carry it before
this change. On this head the wire carries it, and it equals
`RestServer`'s, measured with a scratch test that was not committed
(below). Tightening those rows to the wire is a possible later edit in
`packages/runtime`. Noted, not filed. Carrier: none.
- **Producer-side ablation not run.** That would mean reverting PR
#23628's read and watching the masked-field rows go red. This diff does
not change the producer, so it is not owed here. The rows'
`not.toHaveProperty('secret')` and equality with the caller's read are
what would catch it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent cad343e commit 5bb9a83
7 files changed
Lines changed: 178 additions & 67 deletions
File tree
- .changeset
- content/docs
- api
- protocol/kernel
- packages
- plugins/plugin-hono-server/src
- qa/http-conformance/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
221 | | - | |
| 221 | + | |
222 | 222 | | |
223 | 223 | | |
224 | 224 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
487 | 487 | | |
488 | 488 | | |
489 | 489 | | |
490 | | - | |
491 | | - | |
492 | | - | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
493 | 495 | | |
494 | 496 | | |
495 | 497 | | |
496 | 498 | | |
497 | 499 | | |
498 | 500 | | |
499 | 501 | | |
500 | | - | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
501 | 510 | | |
502 | 511 | | |
503 | 512 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
256 | | - | |
257 | | - | |
| 256 | + | |
258 | 257 | | |
259 | 258 | | |
260 | 259 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
299 | | - | |
300 | | - | |
301 | | - | |
302 | | - | |
303 | | - | |
304 | | - | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
305 | 306 | | |
306 | 307 | | |
307 | 308 | | |
| |||
378 | 379 | | |
379 | 380 | | |
380 | 381 | | |
381 | | - | |
382 | | - | |
383 | | - | |
| 382 | + | |
| 383 | + | |
384 | 384 | | |
385 | 385 | | |
386 | 386 | | |
| |||
412 | 412 | | |
413 | 413 | | |
414 | 414 | | |
415 | | - | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
423 | 424 | | |
424 | 425 | | |
425 | | - | |
426 | | - | |
| 426 | + | |
| 427 | + | |
427 | 428 | | |
428 | 429 | | |
429 | 430 | | |
| |||
434 | 435 | | |
435 | 436 | | |
436 | 437 | | |
437 | | - | |
438 | | - | |
439 | | - | |
440 | | - | |
| 438 | + | |
441 | 439 | | |
442 | 440 | | |
443 | 441 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
219 | | - | |
220 | | - | |
| 219 | + | |
| 220 | + | |
221 | 221 | | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
226 | 226 | | |
227 | 227 | | |
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
231 | 231 | | |
232 | | - | |
| 232 | + | |
| 233 | + | |
233 | 234 | | |
234 | | - | |
| 235 | + | |
235 | 236 | | |
236 | 237 | | |
237 | 238 | | |
238 | 239 | | |
239 | | - | |
| 240 | + | |
240 | 241 | | |
241 | 242 | | |
242 | 243 | | |
| |||
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
277 | | - | |
| 278 | + | |
278 | 279 | | |
279 | 280 | | |
280 | 281 | | |
| |||
285 | 286 | | |
286 | 287 | | |
287 | 288 | | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
| 289 | + | |
| 290 | + | |
292 | 291 | | |
293 | 292 | | |
294 | 293 | | |
| |||
0 commit comments