Skip to content

Commit 4580597

Browse files
os-zhuangclaude
andauthored
fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983) (#4037)
* fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983) The fifth and final drifting route module. Unlike the four in #3843 this one was not found by reading — `scripts/check-route-envelope.mjs` surfaced it the moment that scan went repo-wide, which is the argument for a repo-wide guard over per-package copies. It also turned out to be the one where the drift had broken shipped SDK methods, not merely mis-shaped a body. Three of these routes are `disposition: 'sdk'` in `runtime/src/route-ledger.ts`, and `unwrapResponse` decides a body is an envelope by finding a boolean `success`. With no flag it returns the body verbatim, so against this surface `shareLinks.create()` — documented "Returns the link row (incl. token)" — handed back `{ link: … }` and `.token` was undefined, while `shareLinks.list()`, typed `Promise<any[]>`, handed back `{ links: [] }` and any `.map()` on it threw. `packages/client/src/admin-surfaces.test.ts` mocks all three as `{ success: true, data: <payload> }`: the SDK was written and tested against the DISPATCHER's shape and only ever worked there. So the target shape was not a design decision. `runtime/src/domains/share-links.ts` serves these same five paths — for cloud's per-environment kernels it is the designed PRIMARY surface — and has always answered in the declared envelope with `data` carrying the payload directly. This module now answers identically, which is what makes `unwrapResponse` return the same value either way: POST /share-links { link } -> data: link GET /share-links { links } -> data: link[] DELETE /share-links/:id { ok: true } -> data: { ok: true } GET /:token/resolve { record, link, redact… } -> data: { … } GET /:token/messages { data: rows } -> data: rows errors { error: { code, msg } } -> + success: false `{ ok: true }` survives on revoke, but as the payload rather than as the body: at the top level it was a second word for `success` (#3689 retired that from storage); under `data` it is what the dispatcher already returned. The error half was already nested `{ code, message }` — #3675's changeset cited this module as the good example — so only the flag is new there, and all eleven codes were already SCREAMING_SNAKE and registered, so ADR-0112 needs nothing. Consumers swept: the framework has ZERO. In objectui, ShareDialog was already dual-shape tolerant on all three routes it calls, and carried that tolerance precisely BECAUSE both shapes existed in the fleet. SharedRecordPage needed one fix of the kind a shape-swap would have missed — it renamed the wire's `redactFields` to `redactedFields` only on the bare branch, so on the enveloped dispatcher path the "fields are hidden by the owner" notice never rendered. Converting this surface would have spread that to every share page; fixed in objectui#2980, which merges first. The module had no test file at all (only `share-link-service.test.ts`), so it gains a driven conformance suite: 26 cases over all five routes and thirteen error branches, every body parsed against the real spec schemas, plus the `unwrapResponse` decision restated so the two broken SDK methods are pinned as working. Reverting the two emitters fails 22 of the 26. Guard: 7 conformant / 0 ratcheted / 1 exempt, from 6 / 1 / 1. `privateOk` is also narrowed to what its doc always claimed — a literal `ok` at the TOP of a body, where it competes with `success`; the same literal inside `data` is payload, which is what a conformant revoke returns. Four self-test assertions pin both readings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z * chore(console): bump the objectui pin to 96ee72e so the enveloped resolve body meets a page that renames redactFields (#3983) Step 2 of #3983's merge order, in the same commit range as the conversion so the coupling is atomic: merging the envelope change against a console dist built from the old pin would ship enveloped resolve bodies to a page that only renames `redactFields` -> `redactedFields` on the BARE branch, so the "fields are hidden by the owner" notice would vanish from every share page for one release window. objectui#2980 fixes that and is the pin's new tip. Also fixes `bump-objectui.sh` for the workflow AGENTS.md mandates. Its guard tested `-d "$OBJECTUI_ROOT/.git"`, but in a git WORKTREE `.git` is a regular file holding a `gitdir:` pointer — so the script rejected every linked worktree and only ever ran from a primary clone, while the Prime Directives require a per-task worktree in each repo a task spans. `-e` accepts both. The error message also claimed the default sibling path even when OBJECTUI_ROOT was set explicitly, which pointed the reader at the wrong thing; it now names the path it actually rejected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0166bd5 commit 4580597

7 files changed

Lines changed: 736 additions & 46 deletions

File tree

.changeset/console-96ee72e85439.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `96ee72e85439`. Frontend changes in this range:
6+
7+
- fix(console): render the redaction notice on the enveloped resolve body (objectstack#3983) (#2980)
8+
- feat(sdui): guard the public contract against silent drift (#2979)
9+
- fix(sdui): lazy public blocks reach a kind:'react' page scope; ReactRunner keeps its errors (#2976)
10+
- fix(list,data): bridge every spec view operator onto the filter AST (#2901) (#2974)
11+
- fix(errors): error-code branches survive the framework's ADR-0112 rename (objectstack#3841) (#2977)
12+
- fix(fields): a select no longer wipes itself when its value outruns its options (#2968) (#2969)
13+
- fix(approvals): decision outputs reach both decision surfaces (#2955) (#2961)
14+
15+
objectui range: `e651c936870e...96ee72e85439`
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
"@objectstack/plugin-sharing": patch
3+
---
4+
5+
fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983)
6+
7+
The fifth and final drifting route module. Unlike the four in #3843, this one was
8+
not found by reading — `scripts/check-route-envelope.mjs` surfaced it the moment
9+
that scan went repo-wide, which is the whole argument for a repo-wide guard over
10+
per-package copies. It also turned out to be the one where the drift had actually
11+
**broken shipped SDK methods**, not merely mis-shaped a body.
12+
13+
## Two SDK methods did not work on this surface
14+
15+
Three of these routes are `disposition: 'sdk'` in `runtime/src/route-ledger.ts`,
16+
and `ObjectStackClient.unwrapResponse` decides a body is an envelope by finding a
17+
boolean `success`. With no flag it hands back the body verbatim:
18+
19+
| method | documented / typed as | actually returned |
20+
|---|---|---|
21+
| `shareLinks.create()` | "the link row (incl. `token`)" | `{ link: … }` — so `.token` was `undefined` |
22+
| `shareLinks.list()` | `Promise<any[]>` | `{ links: [] }` — so `.map()` threw |
23+
24+
`packages/client/src/admin-surfaces.test.ts` mocks all three as
25+
`{ success: true, data: <payload> }`. The SDK was written and tested against the
26+
**dispatcher's** shape and only ever worked there.
27+
28+
## This is a convergence, not a redesign
29+
30+
`runtime/src/domains/share-links.ts` serves the same five paths, and for cloud's
31+
per-environment kernels it is the *designed primary* surface
32+
(`registerShareLinkRoutes: false`). It has always answered in the declared
33+
envelope. The plugin now answers identically:
34+
35+
| route | was | now |
36+
|---|---|---|
37+
| `POST /share-links` | `{ link }` | `{ success: true, data: link }` |
38+
| `GET /share-links` | `{ links }` | `{ success: true, data: link[] }` |
39+
| `DELETE /share-links/:idOrToken` | `{ ok: true }` | `{ success: true, data: { ok: true } }` |
40+
| `GET /:token/resolve` | `{ record, link, redactFields }` | `{ success: true, data: { … } }` |
41+
| `GET /:token/messages` | `{ data: rows }` | `{ success: true, data: rows }` |
42+
| errors | `{ error: { code, message } }` | `{ success: false, error: { … } }` |
43+
44+
`data` carries each payload **directly**`data: links`, not `data: { links }`.
45+
That is what makes `unwrapResponse` return the same value on both surfaces, and
46+
it is what the SDK already expected.
47+
48+
## Breaking: raw `fetch` callers add one hop
49+
50+
SDK callers get the fix for free (two of them go from broken to working). Direct
51+
body readers add `.data`:
52+
53+
```diff
54+
- const { links } = await (await fetch('/api/v1/share-links')).json();
55+
+ const { data: links } = await (await fetch('/api/v1/share-links')).json();
56+
```
57+
58+
`{ ok: true }` on revoke survives, but as the payload rather than as the body: at
59+
the top level it was a second word for `success`, which #3689 retired from
60+
storage; under `data` it is what the dispatcher already returned.
61+
62+
The `error` half was already nested `{ code, message }`#3675's changeset cited
63+
this module as the good example of that — so only the `success` flag is new there.
64+
All eleven codes were already SCREAMING_SNAKE and registered, so ADR-0112 needs
65+
nothing.
66+
67+
## Consumers
68+
69+
Swept, and the result is smaller than #3983 assumed. The framework has **zero**
70+
consumers of these routes. In objectui, `ShareDialog` was already dual-shape
71+
tolerant on all three routes it calls (`body.links ?? body.data`,
72+
`created.link ?? created.data`, and revoke never reads the body) — it needs no
73+
change, and it carried that tolerance precisely *because* both shapes existed in
74+
the fleet.
75+
76+
`SharedRecordPage` did need one fix, and it is the kind a shape-swap would have
77+
missed: it renamed the wire's `redactFields` to `redactedFields` only on the
78+
*bare* branch, so on the already-enveloped dispatcher path the "fields are hidden
79+
by the owner" notice never rendered. Converting this surface would have spread
80+
that to every share page. Fixed in objectui#2980, which merges first.
81+
82+
## Guard
83+
84+
**7 conformant / 0 ratcheted / 1 exempt**, from 6 / 1 / 1. The ratchet mechanism
85+
stays for the next module that needs it.
86+
87+
`privateOk` also got narrowed to what its own doc always claimed — a literal `ok`
88+
at the **top** of a body, where it competes with `success`. The same literal
89+
inside `data` is payload, which is what a conformant revoke returns. Four
90+
self-test assertions pin both readings.

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
e651c936870e26ef0c15252baad479d293fb3cf3
1+
96ee72e85439b439fefc8c1f378818d263718dfa

0 commit comments

Comments
 (0)