Skip to content

Commit 092b9da

Browse files
os-litantclaude
andauthored
fix(rest): the package door carries the producer-marked userMessage (#12502) (#12541)
* fix(rest): the package door carries the producer-marked `userMessage` (#12502) `sendThrownError` in `packages/rest/src/package-routes.ts` resolved every throw through the shared ADR-0112 rule and then forwarded `{ details, declaredCode }` and nothing else, so the marked text it was already holding in the same local was dropped one line later. It now spreads `thrown.userMessage` into the envelope writer's `extra` — the third independent spread into that object, and the idiom the dispatcher twin serving this same path (`errorFromThrown`, #9934) already applies. The idiom is the INVERSE of the `declaredCode` half directly above it, and that is the whole reason this is a separate change. `declaredCode` must be read through `demotedDeclaredCode` because its raw field carries a second meaning; it is also set when the producer's spelling IS the registered member. `userMessage` has no second meaning — `declaredUserMessage` already decided what counts as marked — so the caller passes the raw field straight through and invents no obligation to match the sibling. Additive: an unmarked refusal still carries none, and the three shapes `declaredUserMessage` rejects (empty, whitespace-only, non-string) still carry none, so nothing invents a mark for a producer that never wrote one. `details`, `declaredCode`, `code`, `status` and `message` are unchanged on every existing path, and the 5xx prose withhold never sees the marked channel. The reachability standard this door is now judged on is stated in the changeset, in the new suite's docblock and in the PR body rather than left implicit: the in-tree producer set here is empty for BOTH channels, and the live population is the injected/federated limb this door composes rather than closes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd * docs(changeset): correct one stale clause in the `declaredCode` changeset (#12502) `.changeset/package-door-declared-code.md` closed with "the shared `sendError`'s `extra` does not admit it yet". That clause was already false when it merged: PR #12477 (#12404, `0e5fe7fc0`) widened the `extra` Pick to admit `userMessage` and landed BEFORE `14b114553`. Both changesets are unreleased and compile into the same CHANGELOG, so this fixes a document before it is published rather than rewriting history — and it is fixed in place, because a correction sitting beside the false sentence would ship both to the reader. One clause. The rest of that changeset's prose is untouched, and no other changeset is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3a3a099 commit 092b9da

4 files changed

Lines changed: 541 additions & 1 deletion

File tree

.changeset/package-door-declared-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ scoped to the prose by its own contract (`status`, `code` and `details` untouche
3838
either.
3939

4040
`userMessage` is deliberately NOT threaded here — the shared `sendError`'s `extra`
41-
does not admit it yet, and that channel is tracked separately.
41+
has admitted it since #12404, and that channel is threaded separately, in #12502.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): the direct-mount package door carries a producer-marked `userMessage` (#12502)
6+
7+
`GET /api/v1/packages`, `GET /api/v1/packages/:id`, `POST /api/v1/packages/publish`
8+
and `DELETE /api/v1/packages/:id` now put a producer's user-facing refusal text on
9+
the wire's `error.userMessage` when the throw carried one. Previously that text was
10+
resolved and then dropped: `sendThrownError` (`packages/rest/src/package-routes.ts`)
11+
asked `resolveThrownHttpError` for the answer — which returns `userMessage` exactly
12+
when the producer marked a non-empty string at throw time (#9934) — and then
13+
forwarded only `details` and `declaredCode` to the shared envelope writer.
14+
15+
Nothing invalid shipped, which is what made the loss silent: `code`, `status` and
16+
`message` were all correct, so every body parsed, while an author's deliberate,
17+
end-user-addressed sentence vanished and a consumer told by ADR-0112 to render
18+
`userMessage` verbatim found nothing there and fell back to its generic
19+
substitution — the #3821 behaviour this channel exists to override.
20+
21+
This completes the pair the sibling change left open. That one threaded
22+
`declaredCode` and said `userMessage` was "tracked separately"; this is it, and the
23+
`extra` it spreads into has admitted the field since #12404.
24+
25+
⛔ The idiom is the INVERSE of the sibling's, deliberately. `declaredCode` must be
26+
read through `demotedDeclaredCode` because its raw field carries a second meaning —
27+
it is also set when the producer's spelling IS the registered member, so forwarding
28+
it raw would put two spellings of one fact on every registered refusal.
29+
`userMessage` has no second meaning: `declaredUserMessage` already decided what
30+
counts as marked (a non-empty string, or nothing), so the caller passes
31+
`thrown.userMessage` straight through, byte for byte what the dispatcher twin
32+
serving this same path does (`errorFromThrown`,
33+
`packages/runtime/src/http-dispatcher.ts`). Consumers must not read presence as
34+
anything but "the producer opted in".
35+
36+
Additive and shape-preserving. An unmarked refusal still carries no `userMessage`,
37+
and the three shapes `declaredUserMessage` rejects — `''`, whitespace-only, a
38+
non-string — still carry none, so nothing invents a marked message for a producer
39+
that never wrote one. `details`, `declaredCode`, `code`, `status` and `message` are
40+
byte-identical to before on every existing path.
41+
42+
The 5xx message withhold is unchanged and does NOT suppress the mark: that withhold
43+
rewrites a local `message` const and `looksLikeInternalErrorLeak` is only ever handed
44+
`thrown.message`, so the marked channel is never an input to it. The two are
45+
answering different questions — leaked diagnostic prose is withheld, while the
46+
producer's own sentence to the end user discloses only what it chose to — and the
47+
ruling that created the channel made it status-agnostic on purpose.
48+
49+
⚠️ Stated because it is the honest cost, and so the next channel added here does not
50+
have to rediscover which bar applies: the IN-TREE producer set at this door is empty,
51+
for this channel and for `declaredCode` alike. This door is judged live because it is
52+
**composed rather than closed**`resolvePackageService()` and the `protocol` slice
53+
are open composition points whose throws all four handlers forward verbatim, and
54+
ADR-0112's federation amendment exists precisely because the producer set is not
55+
enumerable in-tree. The live population is the injected/federated limb, which is the
56+
population the new pins in `packages/rest/src/package-door-user-message.test.ts`
57+
drive.

0 commit comments

Comments
 (0)