fix(plugin-sharing)!: the share-link routes emit the declared envelope, and the last ratchet retires (#3983) - #4037
Merged
Conversation
…e, 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…olve 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
os-zhuang
marked this pull request as ready for review
July 30, 2026 05:51
This was referenced Jul 30, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3983. The fifth and final drifting route module — guard goes 6 / 1 / 1 → 7 / 0 / 1.
Merge order is satisfied inside this PR. objectui#2980 is merged (
96ee72e) and the.objectui-shabump is a commit here, so the coupling is atomic — see Merge order.This wasn't envelope cosmetics — two SDK methods were broken
Worth leading with, because #3983 framed this as "no body carries the
successflag" and that undersells it.Three of these routes are
disposition: 'sdk'inruntime/src/route-ledger.ts, andObjectStackClient.unwrapResponsedecides a body is an envelope by finding a booleansuccess. With no flag it hands back the body verbatim:shareLinks.create()token)"{ link: … }— so.tokenwasundefinedshareLinks.list()Promise<any[]>{ links: [] }— so.map()threwAnd
packages/client/src/admin-surfaces.test.tsmocks 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 wasn't a design decision
runtime/src/domains/share-links.tsserves these same five paths, and for cloud's per-environment kernels it is the designed primary surface (registerShareLinkRoutes: false). It has always answered in the declared envelope withdatacarrying the payload directly. Same routes, two shapes, decided by which surface happened to mount them — the asymmetry #3636 fixed for/i18n, one domain over. This module converges onto the twin:POST /share-links{ link }{ success: true, data: link }GET /share-links{ links }{ success: true, data: link[] }DELETE /share-links/:idOrToken{ ok: true }{ success: true, data: { ok: true } }GET /:token/resolve{ record, link, redactFields }{ success: true, data: { … } }GET /:token/messages{ data: rows }{ success: true, data: rows }{ error: { code, message } }{ success: false, error: { … } }datacarries each payload bare —data: links, notdata: { links }. That is what makesunwrapResponsereturn the same value on both surfaces.Two details worth calling out:
{ ok: true }survives on revoke, but as thedatarather than as the body. At the top level it was a second word forsuccess(service-storage success bodies are three shapes, none carryingsuccess: true— the other half of #3675 #3689 retired that from storage); underdatait is the payload, and it is the payload the dispatcher andadmin-surfaces.test.tsboth already use. Dropping it would have re-introduced a divergence.{ code, message }, which is why service-i18n error responses are bare{ error }, not the declared{ success: false, error: {...} }envelope #3675's changeset cited this module as the good example. Only the flag is new, and all eleven codes were already SCREAMING_SNAKE and registered, so ADR-0112 needs nothing here.Consumer sweep
#3983 said this "needs its own consumer sweep". Done, and the blast radius is smaller than assumed — the framework has zero consumers of these routes (no dogfood proof drives them either).
In objectui,
ShareDialogwas already dual-shape tolerant on all three routes it calls —body.links ?? body.data,created.link ?? created.data, and revoke never reads the body. It needs no change, and the reason it was already tolerant is the point: both shapes genuinely exist in the fleet.SharedRecordPagedid need one fix, and it is exactly the kind a shape-swap would have missed. It renamed the wire'sredactFieldsto theredactedFieldsits render reads — but only on the bare branch. The enveloped branch handedbody.datathrough verbatim, so on the dispatcher path the "Some fields are hidden by the owner" notice never rendered, on exactly the pages where fields were being stripped. Converting this surface would have spread that to every share page. Fixed in objectui#2980 (merged).Merge order
96ee72e.objectui-shabump + console rebuild → commit3a67ab4in this PRStep 2 lives here rather than in its own PR so the coupling is atomic: merging the conversion against a dist built from the old pin would ship enveloped resolve bodies to a page that skips the rename, i.e. it would cause the vanished-notice regression for one release window.
check:console-shaconfirms the rebuilt dist matches the new pin (packages/console/distis gitignored, so only.objectui-shaand the generated changeset are committed).I'm stating the order explicitly because I got the analogous claim wrong on #3972 ("the two repos are not coupled by merge order") —
packages/console/distis built from.objectui-sha, so they are.Bonus fix:
bump-objectui.shrejected the mandated workflowDoing step 2 surfaced a defect in the tool for it. The script guarded on
-d "$OBJECTUI_ROOT/.git", but in a git worktree.gitis a regular file holding agitdir:pointer — so it 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.-eaccepts both. Its error message also claimed the default sibling path even whenOBJECTUI_ROOTwas set explicitly, pointing the reader at the wrong thing; it now names the path it actually rejected.Tests
The module had no test file at all — only
share-link-service.test.tsbeside it. It gains a driven conformance suite: 26 cases over all five routes and thirteen error branches, every body parsed against the real spec schemas rather than restatements of them, plus theunwrapResponsedecision restated so the two broken SDK methods are pinned as working, and asameShapeAsDispatcherblock so the two surfaces cannot drift apart again (#3833's failure mode, one domain over).I checked the suite isn't vacuous: reverting the two emitters to their pre-#3983 form fails 22 of the 26.
Guard
privateOkis narrowed to what its own doc comment always claimed — a literalokat the top of a response body, where it competes withsuccess. The same literal insidedatais payload, which is what a conformant revoke returns; counting it was an over-broad approximation that was harmless only while no conformant module had one. Four self-test assertions pin both readings, and the narrowing can only lower counts, so the six already-conformant modules stay atprivateOk: 0.The ratchet section is now empty but the mechanism stays — it is how the next drifting module gets recorded honestly instead of being either fixed on the spot or quietly skipped.
Verification
pnpm check:route-envelope+--self-testpnpm check:console-shaobjectui@96ee72e85439@objectstack/plugin-sharing@objectstack/runtime@objectstack/clienttsc --noEmit·check:docs·check:skill-refs·check:react-blockscheck:error-code-casing·check:nul-bytes·check:role-word·check:org-identifiereslint --no-inline-configon changed filesMethod note. The runtime suite first showed 1 failure in
datasource-autoconnect.test.ts(expected ['first','second'], got each row 6×). It is not mine and not a product bug: a gitignoredpackages/runtime/.objectstack/data/memory-driver.jsonpersists rows across local runs. I confirmed it by stashing my changes and reproducing on the pristine tree, then cleared the file and got 904/904 both with and without my diff. CI checks out fresh, so it never sees it. Recording the method because I misdiagnosed a stale-artifact failure as "reproduces on main" during #3972.Deferred finding. #4038 — the dispatcher domain emits
dataand a legacylink/linksbeside it on create/list. That producer-side shim loses its last reader once this PR lands, but deleting it needs acloudsweep, so it is filed rather than done here (Prime Directive #10).