Skip to content

fix(publish): sync restricted access list from server so "Change" can't silently wipe it - #491

Open
StpMax wants to merge 3 commits into
stagingfrom
fix/ENG-931
Open

fix(publish): sync restricted access list from server so "Change" can't silently wipe it #491
StpMax wants to merge 3 commits into
stagingfrom
fix/ENG-931

Conversation

@StpMax

@StpMax StpMax commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

Editing an already-shared artifact's restricted access list via "Change" read the email list from local component state that was only ever populated by the immediate response of a same-session publish() call. For an already-published artifact opened any other way — a page reload, a new session/device, or a chat-bubble preview whose artifact object is synthesized from stream steps — the list silently reset to empty, even though the server still had the real list stored.

If the user then clicked "Change" → made any tweak → "Update", the empty/stale local list was submitted and overwrote the real server-side access list, revoking everyone who actually had access. This is a data-loss bug, not a display bug.

Fixes ENG-931. Unblocks ENG-930 (restricted-access summary needs this data to exist client-side first).

Root cause

  • usePublish seeded accessEmails/orgAllowed purely from the artifact prop and never fetched them from the server for an existing artifact.
  • refresh() called /artifacts/status but consumed only modified/publishedUrl/artifactKey — it dropped the accessMode/accessEmails/orgAllowed fields the endpoint already returns.
  • The only place these ever got real values was the publish() response — good for that one session, gone the moment the viewer closed and reopened.

Note: the server side already returns the access list (and deliberately strips the plaintext password) via /artifacts/status — it has since June, well before this ticket. So the fix is client-only: consume what the server already sends.

How

All changes are in usePublish.js and PublishMenu.jsx; no server or API-signature changes.

  • refresh() now consumes the access fields (accessMode/accessEmails/orgAllowed) from /artifacts/status, and is called on viewer open for every artifact type — so a synthesized (chat-bubble) or stale prop is corrected from the server before it can be edited. /artifacts/status is used rather than the preview-mount payload because it is type-agnostic (covers .md, which bypasses mount) and omits the plaintext password.
  • onChange from refresh() now carries the access fields back into the artifact prop. This is load-bearing: the broad re-sync effect re-seeds accessEmails from the prop whenever publishedUrl/accessMode change, so dropping the fields there would immediately self-clobber the list we just loaded.
  • New accessLoaded flag (seeded from whether the prop carries authoritative access — accessMode/accessProtected from a listing — via a shared hasServerAccess() helper). While the real prior list has not loaded, "Update" in the Change-access view is disabled ("Loading current access…"), with a matching guard in doApplyAccess, so an unconfirmed list can never be submitted. The flag is reset per-artifact-identity (keyed on targetPath), not in the broad re-sync effect, so onChange-driven prop churn can't leave it stuck.
  • Late-arriving data re-seeds the draft. If the panel was opened before the server list landed (draft seeded empty), the Change-access draft is re-seeded when the real list arrives — but never over edits the user already started (draftDirty).

Behavior by entry point

Entry point Before After
Artifacts grid / working-folder rail List correct (prop from listing carries it) Unchanged; "Update" usable immediately
Chat bubble (synthesized object) List empty → "Update" wipes server list List loaded from server before editing; "Update" blocked until confirmed
Reload / new session Could reset to empty Always shows the true saved list

Tests

  • usePublish.test.jsx (new): refresh() consumes access even when modified/publishedUrl are unchanged; open-refresh fills the list for a prop lacking it; accessLoaded seed (incl. legacy accessProtected-only) and reset; self-clobber roundtrip (onChange result fed back into the prop must not wipe the loaded list); same-session publish(restricted) still shows the new list immediately.
  • PublishMenu.test.jsx (extended): enabled "Update" for a loaded restricted artifact (grid/rail); "Update" blocked while unloaded (chat-bubble guard); draft re-seeds when the real list arrives after "Change" was opened.

Verification

  • npm test → 510/510 passing
  • npm run typecheck → clean
  • npm run check:cowork-purity → clean

Notes / out of scope

  • Known limitation (fail-safe): if /artifacts/status returns null (transient network error, or an old server without the endpoint), accessLoaded stays false for a synthesized artifact and "Update" remains disabled rather than risking an overwrite. On a supported server this recovers on the next window focus; a dedicated failed/retry state was intentionally not added.
  • Merged latest staging (resolved one conflict: FooterButton adopts staging's shared Button component while keeping the title prop used by the loading guard).

Type of change

Please delete options that are not relevant.

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ⚡ New feature (non-breaking change which adds functionality)
  • 🚨 Hotfix (non-breaking change which fixes an issue)
  • 📢 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📄 This change requires a documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have checked my code and corrected any misspellings

StpMax added 3 commits July 24, 2026 10:55
# Conflicts:
#	src/renderer/cowork/components/artifact/publish/PublishMenu.jsx
@StpMax
StpMax requested a review from ea-rus July 24, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant