Repository navigation
Commit 19c554d
Part of #23531
Clause-②: no
This lands the card's `currentVersion` half. Its `currentRecord` half
stays on the card, ordered after #23586 (patch round 1, the seat's
review `6182753773`).
## What this does
A stale `If-Match` on `PATCH` or `DELETE /data/:object/:id` is refused
by the protocol with `ConcurrentUpdateError` (`409 CONCURRENT_UPDATE`).
`RestServer` answers it with the server's `currentVersion` (and
`currentRecord`). Both Hono doors answered it with `code` and `message`
only: `@objectstack/hono`'s `createHonoApp` (its `dispatch()` catch-all,
through `thrownJson`) and a route mounted on `HonoHttpServer` (`wrap()`,
and the transport `onError` seam). All three paths render through one
function, `declaredEnvelopeForThrow` in
`packages/plugins/plugin-hono-server/src/adapter.ts`. That function
forwarded only the `details` that the thrown-error resolver derives, and
it never reads the two fields, because they are own properties of the
error.
`declaredEnvelopeForThrow` now asks one helper beside it,
`declaredConflictContext`. For a `409 CONCURRENT_UPDATE`, that helper
reads the conflict's context off `structuredCodeAnswer`
(`@objectstack/types`, already exported). That is the arm `RestServer`'s
`mapDataError` answers this conflict through. Everything the arm's body
carries is forwarded except three keys:
- its own sentence and code, which the envelope already carries as
`message` and `code`;
- `currentRecord`, which is held (next section).
So:
- ⛔ no third spelling of the conflict body. The one field the Hono
package names is the one it holds back, and it names it only to hold it.
Everything else appears only when the producer stated it, by the REST
arm's own rule;
- the arm must agree with the envelope already decided (same status,
same code), so it can only add context and never changes the answer;
- only `CONCURRENT_UPDATE` is taken, per the card's scope ruling. Other
structured arms are listed under Acceptance notes.
**Where it lands:** `error.details`. On the wrapped envelope (`{
success, error }`) that is the declared context slot
(`ApiErrorSchema.details`), and it is where `@objectstack/client` reads
a wrapped body's context (`error.details = errorBody.details ??
errorBody.error.details ?? errorBody`). That is what objectui's
`normaliseClientError` reads `currentVersion` from. The REST door's flat
body has no envelope, so it carries the field at the top level.
- FROM (measured on this branch's pins before the fix): `409
{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"Record
task/ID was modified by another user (current version T1, expected
T0)"}}`
- TO: the same, plus `"details":{"currentVersion":"T1"}`
Status, `code` and `message` do not move. A throw that is not a `409
CONCURRENT_UPDATE` renders byte-identically to before. The REST door's
answer is unchanged.
**Both doors pick the change up from `adapter.ts` alone:**
`createHonoApp`'s `thrownJson` (`packages/adapters/hono/src/index.ts`,
not edited here), `wrap()` through `runHandler`, the `onError` seam, and
the metrics observer's status read all call `declaredEnvelopeForThrow`.
## `currentRecord` is held on these doors (#23586)
The protocol reads the conflict's current row with `probeRecord`, which
is `engine.findOne(object, { where: { id }, context: { isSystem: true }
})` (`packages/metadata-protocol/src/protocol.ts`). `assertVersionOf`
throws that row as `currentRecord` before `engine.update` /
`engine.delete` apply the caller's write policy, and no layer narrows
it. So the field may carry a row, or fields, that row- or field-level
security hides from the caller. That is filed as #23586 (a security
exception, not measured at a door). Forwarding it here would widen that
possible disclosure to two more doors.
A version token carries no row data, so `currentVersion` lands now.
`declaredConflictContext` excludes `currentRecord` by name, with a
pointer to #23586 at the exclusion, and the pins assert it ABSENT at
both doors. The exclusion is lifted once the producer's answer lands.
`RestServer` still forwards the field. That is the producer's question
in #23586, and this PR does not touch that door.
## Tests
Final head `69215f8575`.
- **Pins** (both files keep their controls):
-
`packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts`,
the `[#23531]` block. A direct-mount `PATCH` and `DELETE` route judges
`If-Match` and throws a `ConcurrentUpdateError`-shaped refusal that
states both fields. The pins assert `error.details` equals `{
currentVersion }` exactly, that it has no `currentRecord`, and that the
row's values appear nowhere in the body.
-
`packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts`,
the `[#23531]` block. A real `createHonoApp` over the real dispatcher
and protocol (SQLite WASM), with stale `If-Match` on `PATCH` and
`DELETE`. `currentVersion` is proved by use: echoed as the next
`If-Match`, it is accepted (`PATCH` writes, `DELETE` deletes).
`currentRecord` is asserted absent, although the real protocol states it
(round 0 read it on this rig as `{ id, name }`).
- **Red before the fix** (round 0, pins only, `13b5267b42`): unit `Tests
2 failed | 21 passed (23)`, conformance `Tests 2 failed | 13 passed
(15)`. The body then had no `details`.
- **Ablation, round 1**, on `69215f8575`:
- Method: `node scripts/ablation-replace.mjs` in wrap mode, plus an
outer `trap … EXIT INT TERM` restore against the absolute path. Each leg
was rebuilt, and `ablation-dist-preflight.mjs` showed the marker in
`dist/index.js` and `dist/index.mjs`.
- **Leg A** dropped the exclusion: anchor ``&& key !==
'currentRecord'),`` became a key no body carries. The absence pins went
RED, unit `2 failed | 21 passed` (`expected { …(2) } to deeply equal {
Object (currentVersion) }`) and conformance `2 failed | 13 passed` (`to
not have property "currentRecord"`).
- **Leg B** disabled the guard: anchor ``if (resolved.code !==
'CONCURRENT_UPDATE') return undefined;`` was made unmatchable. The
`currentVersion` pins went RED, unit `2 failed | 21 passed` (`expected
undefined to deeply equal …`) and conformance `2 failed | 13 passed`
(`expected 'undefined' to be 'string'`).
- On a first run, leg B's conformance red came from a `TypeError`
instead: the absence assertion ran on an undefined `details` before the
`currentVersion` check. The assertions were reordered (`69215f8575`) and
both legs re-run, with the result above.
- **Restore:** blob equal to HEAD, `git diff HEAD` empty, `git status`
clean. After a rebuild, preflight `--absent` found both markers gone
from all 6 built files. Both suites were green again.
- **Packages** at `69215f8575`:
- `pnpm --filter @objectstack/plugin-hono-server typecheck` (tsc,
`tsconfig.typecheck.json`, and `check:test-typecheck` OK with 0 debt)
and `test`: `Test Files 29 passed`, `Tests 340 passed`.
- `pnpm --filter @objectstack/http-conformance typecheck`
(`check:test-typecheck` OK, ledger unchanged) and `vitest run`: `Test
Files 13 passed`, `Tests 139 passed`.
- Both ran after a rebuild of `http-conformance`'s dependency closure,
`plugin-hono-server` included.
- `@objectstack/hono`'s own suite is not run locally: its vitest config
stubs `@objectstack/runtime`, its source is untouched, and the
composed-host pin above covers its behaviour. It is declared to CI.
- `git merge-tree --write-tree HEAD origin/main` was clean at the start
of the round (one docs commit ahead), so the branch was not merged
again. The round-0 merge (`9d467b933a`) stands.
## Gates
Local, at the final head `69215f8575`:
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived **66** commands from the 4 changed
paths (merge base `fa6a6085c`), the same 66 as round 0. All 66 ran, each
exit code captured before any pipe: **66 x exit 0**.
- `pnpm check:dual-build-cjs-loads` first exited 3, `PREREQUISITE NOT
MET`: this round's fresh worktree had no workspace `dist/`. After a
cache-backed `pnpm turbo run build` (`75 successful`, `74 cached`) it
measured: `110 published require entry point(s) across 68 package(s)
load`.
- `--ran` reconciliation: `66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN`
(a derived zero; every line carries its exit code).
- The four artifact-roster gates the derivation marks as keeping their
roster in a directory this diff touches, all exit 0: `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing`, `pnpm check:filter-alias-parity`.
- Lint: `pnpm lint` is the repo-level scan CI owns (`Lint · ESLint`).
Narrowed locally to the three changed sources with the same flags
(`eslint --no-inline-config`): exit 0, with 0 errors and 0 warnings
each. The ESLint config is unchanged between the merge base and
`origin/main`.
- Every family CI runs on this PR is CI's to report on its check-runs.
## Changeset
`.changeset/23531-hono-conflict-body.md`:
`@objectstack/plugin-hono-server` `patch` and `@objectstack/hono`
`patch`. The TO body is `details.currentVersion` only, and one sentence
says `currentRecord` is held until #23586 and that the REST door's
answer is unchanged. `@objectstack/hono` is named because
`createHonoApp`'s answer is the measured door, and its CHANGELOG carries
the #23365 entry that states this gap. Nothing in its own files changes,
and both packages are in the one fixed version group, so naming it moves
no version.
## Acceptance notes
- **#23586:** `currentRecord` is a system-context row, thrown before the
caller's write policy runs. It is held on these two doors until the
producer's answer lands, and this card's `currentRecord` half is ordered
after it.
- **#23587:** the same family, beyond this card. A real `createHonoApp`
answered a restricted `DELETE` with `409 DELETE_RESTRICTED` and without
the `developerMessage`, `dependentObject`, `dependentCount` and `object`
that `RestServer`'s arm carries (measured at `d7484b8621`). Not
measured: `METADATA_CONFLICT`'s `currentVersion`, and the engine's
`INVALID_FIELD`. Widening `declaredConflictContext` from one code to
"the arm agrees on status and code" is the mechanical shape.
- `object` is not forwarded: `declaredEnvelopeForThrow` has no route
context, so the arm is asked without one, and the arm adds `object` only
when given one.
- The adapter-side file surface is `declaredEnvelopeForThrow`, the
helper beside it, and one added name on the existing
`@objectstack/types` import. No new export.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1a9d488 commit 19c554d
4 files changed
Lines changed: 259 additions & 1 deletion
File tree
- .changeset
- 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 | + | |
| 18 | + | |
| 19 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
| |||
291 | 292 | | |
292 | 293 | | |
293 | 294 | | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
294 | 303 | | |
295 | 304 | | |
296 | 305 | | |
| |||
318 | 327 | | |
319 | 328 | | |
320 | 329 | | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
321 | 335 | | |
322 | 336 | | |
323 | 337 | | |
| |||
330 | 344 | | |
331 | 345 | | |
332 | 346 | | |
333 | | - | |
| 347 | + | |
334 | 348 | | |
335 | 349 | | |
336 | 350 | | |
337 | 351 | | |
338 | 352 | | |
339 | 353 | | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
340 | 424 | | |
341 | 425 | | |
342 | 426 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
217 | 316 | | |
218 | 317 | | |
219 | 318 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
243 | 299 | | |
244 | 300 | | |
245 | 301 | | |
| |||
0 commit comments