Uh oh!
There was an error while loading. Please reload this page.
fix(runtime): stop publish-drafts disclosing driver and subscriber text on unhideError and rebindError - #8816
Conversation
…-drafts' unhideError and rebindError (#8516)
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
qq9340100
commented
Aug 15, 2026
ACCEPT — PM review, |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8516
The two remaining undeclared-text fields on the
POST /packages/:id/publish-drafts200 body — the same response whoseseedAppliedfield #8443 converted. Both closed, so this closes the card.Honest framing, per the card: this is over-disclosure of driver/subscriber text on a success body, not a privilege escalation. Nothing here grants access; what it did was hand internal sentences to a caller who was already authorized to publish.
Reproduced first, as the card required
The card was explicit that both sites were read from source, not driven end to end — found by grepping for the string shape #8443's site used. Both were driven for real through
HttpDispatcher.handlePackagesbefore anything was changed, and both reproduced. Measured onorigin/main:getMetaItemsthrows (the flip's app read)"unhideError": "SQLITE_ERROR: no such table: sys_metadata"saveMetaItemthrows on app 2 of 2"unhiddenApps": ["crm"]"rebindError": "TypeError: Cannot read properties of undefined (reading 'triggers') at AutomationPlugin.rebind (/srv/objectstack/packages/services/service-automation/dist/index.js:412:31)"The announce is the worse of the two and the one no boundary could ever have caught: an internal stack frame plus a server filesystem path and the shipped module layout. Both fields ride a success body as data, so no HTTP boundary's 5xx message withhold can reach them — the disclosure had to be closed at the producer.
The fix
The rule is imported, never re-spelled:
clientFacingFailureTextfrom@objectstack/metadata-protocol, the one implementation #8443 exported, called at both sites. A caught sentence is quoted only when the error declared itself a 4xx client refusal (ADR-0112); anything else gets the stable sentence the field could already carry, and the original goes to the server log.Both halves of the rule, because the two sites started in different states. The flip already logged its cause in full at
errorwith an operator remedy, so only its payload changed and that log is untouched. The announce had no log line at all — withholding alone would have converted an over-disclosure into a silent failure, which is worse — so it gains one naming the cause, the concrete consequence (a newly published record-triggered flow does not bind its trigger until the process restarts) and the fix (re-run the idempotent publish, or restart).That new line is
warn, noterror, on three independent grounds: nothing here claimed to persist and did not (the drafts are published, the flip is stored — what is lost is an in-memory re-sync); AGENTS.md's own worked example of a functional degradation is verbatim "a trigger is not armed"; and the sibling announce of this same event,MetadataPlugin._reloadAndAnnounce, already logs it atwarn.announceKernelEventis not in thecheck:durability-log-levelvocabulary, and escalating would be the over-application AGENTS.md warns trains everyone to skimerror.The authored population — measured, not inherited
The judgement that decided the shape of #8333 and #8443: does either catch receive an authored population that declares nothing and would be blanked?
unhideError: yes, and it declares.saveMetaItem's refusals all carry 4xx —NOT_OVERRIDABLE/403,ITEM_LOCKED/403,OBJECT_OVERLAY_PACKAGE_MISMATCH/422, the org and destructive-change refusals — verified at each helper, and measured through the door: a locked app still tells its publisher which app and why, verbatim. The withhold blanks none of it, and theunhiddenAppshalf-flip report beside it is untouched.rebindError: no.context.triggerdispatch is PROPAGATING, and all three subscribers ofmetadata:reloadedare platform code doing internal re-sync —resyncFlowsFromProtocol,resyncAuthoredHooks/...Actions,ingestReloadedObjects, the authored translation sync. What arrives is internal text an author cannot act on. A guard case pins the bound anyway: if a subscriber ever does declare 4xx, the positive list quotes it with no change here.So no fix was needed at the producer end, and no
safeParse-style declaration was called for — unlike #8443, neither catch has a population that had to be made quotable.Tests
packages/runtime/src/domains/packages-flip-announce-disclosure.test.ts, 8 cases: a positive control, the three withholds, both halves of the log rule, and two[GUARD]cases pinning that a declared 4xx refusal is still quoted verbatim.Reverse verification, both directions predicted before running:
domains/packages.tsreverted toorigin/main— predicted 4 failed | 4 passed, measured 4 failed | 4 passed, every red failing on the text (expected 'SQLITE_ERROR: no such table: sys_metadata' to be 'visibility flip failed').Three pre-existing pins in
http-dispatcher.test.tsasserted the raw driver text onunhideError. Each was triaged individually and re-spelled, not weakened: every one has a live subject that survives untouched (reports-rather-than-throws; theerrorlog's consequence + fix; the #5242 mid-loop split report), and in each the injected error is a bare undeclaredError— the population the rule withholds. The driver sentence is still asserted in each, now in the log line where it belongs.Swept out-of-package for anything pinning these two field names or this response's shape: only CHANGELOG prose and one same-package success-path assertion. No consumer pins either field.
Verification
All at
ae47a64d5, the final commit:pnpm --filter @objectstack/runtime test— 158 files, 2401 tests, all passing;typecheckclean.scripts/pm/dispatch-gates.mjs, all green:check:nul-bytes,check:route-envelope,check:cross-package-test-inputs,check:changeset-gate-self-tests,check:objectui-changeset,check:query-options-erasure,check:type-check-coverage,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset, pluscheck:durability-log-leveladded because this diff adds a log line inside acatch.check:type-check-debtfirst refused to run (unbuilt closure — NOT MEASURED, not a pass). The full closure was built aslint.ymldoes and it was re-run: green, 33 ledger entries re-measured, none above its recorded number. Its one informational note (@objectstack/lintcould be lowered by 1) is pre-existing and in a package this PR does not touch.Generated by Claude Code
Generated by Claude Code