Skip to content

FM-SYNC-HARDENING: wire visibility settings, reconnect re-pull, fail-closed permissions#61

Merged
keyxmakerx merged 5 commits into
mainfrom
claude/fm-sync-hardening
Jun 5, 2026
Merged

FM-SYNC-HARDENING: wire visibility settings, reconnect re-pull, fail-closed permissions#61
keyxmakerx merged 5 commits into
mainfrom
claude/fm-sync-hardening

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Replaces #60 (whose branch had a stale base that couldn't be cleanly rebased through the available tooling). Same change set, on a branch that descends directly from mainconflict-free.

What landed

Decision-free hardening of the sync module's permission model + reconnect path. No new features (no worldState/calendar live-sync — that's Phase 5).

§1 (HIGH) — dead visibility settings wired in

dmOnlyHidden + defaultOwnership were registered and written by the dashboard but no sync module read themjournal-sync._buildOwnership hardcoded its own mapping. The operator toggled controls that silently did nothing.

  • New shared helper scripts/_ownership.mjs (defaultLevelForVisibility / configuredDefaultOwnership) is the single source of truth, consumed by both journal-sync._buildOwnership and note-sync._buildNoteOwnership.
  • dmOnlyHidden ON (default) → DM-only/private content is NONE for players; OFF → it surfaces at the configured default level.
  • defaultOwnership now drives the player-visible level.

§1 wire-vs-remove decision: WIRED. To keep wiring non-breaking, the registered default of defaultOwnership was changed 0 → 2 (OBSERVER): the value was never read before while the code hardcoded OBSERVER, so 2 is the always-effective default. Worlds that never touched it behave identically; operators who set it are now honored.

syncPermissions (third control in the same Permissions section) is also dead config — left out of scope (dispatch named only the two) and flagged as a follow-up, since gating the whole derivation on it is a security-relevant behavior change.

§2 (HIGH) — reconnect re-pull

_initialSyncDone was a one-shot latch, so Chronicle edits during a WS disconnect were lost until a world reload. SyncManager now drives a debounced re-pull off the connection state machine (onStateChange → _onConnectionStateChange → _scheduleReconnectResync → _resyncAfterReconnect). Genuine reconnects (after initial sync) re-run the pull; the first connect is untouched; flapping collapses to a single re-pull after 3s stable. The pull is the existing delta (/sync/pull?since=lastSyncTime).

Delta-vs-full tradeoff (flagged): the mapping pull is delta, but per-module onInitialSync (e.g. notes) is still full. Debounced + reconnect-only keeps it bounded; delta-aware module syncs are a follow-up.

§3 (MED, security) — custom-visibility fails CLOSED

_buildOwnership's custom-visibility error path fell back to is_private ? NONE : OBSERVER — a GM-restricted custom entity with is_private=false became player-visible on a transient permissions-API error. Now returns NONE unconditionally. Fail closed, never open.

§4 — while-in-here

  • Per-user grants (MED): custom-visibility subject_type:"user" grants map to specific Foundry users via the user-mapping table (getFoundryUserId); unmapped users dropped (under-share, no leak).
  • Silent push failures (MED): journal + note create/update failures now surface via ui.notifications.warn (already in the dashboard error log via _logError); idempotent PUT updates enqueue for retry on reconnect.
  • module.json (MED): download URL v0.1.0 pin → latest pattern so a real release doesn't 404; the release workflow still overrides both for tagged releases.
  • Legacy API field names (MED) — FLAGGED, not changed: couldn't be live-verified against a running Chronicle from the dev sandbox. The existing .ai.md "Known API Field Discrepancies" table already documents the aliases; a one-paragraph reframe marking them explicitly load-bearing is a trivial follow-up (omitted to avoid a 50KB+ doc round-trip through the file-push tooling).
  • LOW (flagged, not changed): single non-keyed _syncing boolean; player-notes page forced OBSERVER; pre-release 0.1.0 + manual-only release workflow.

Automated gates

  • node --test tools/test-*.mjs392/392 (363 baseline + 21 new ownership + 8 new reconnect).
  • New tools/test-build-ownership.mjs (§1/§3/§4 + static-source pins incl. the fail-open regression pin) and tools/test-sync-manager-reconnect.mjs (debounce / flap / stop + pins).
  • node tools/check-package-descriptor.mjs → OK. node --check clean on all changed scripts.

Manual verification (see TESTING.md)

  • Visibility toggle: GM toggles dmOnlyHidden → players lose/gain dm-only content on next sync; defaultOwnership = Owner/None changes the player-visible level.
  • Fail-closed: block /permissions for a custom-visibility entity → players cannot see it (NONE), even with is_private=false.
  • Reconnect: disconnect Chronicle → edit an entity → reconnect → change appears (~3s) with no world reload; flapping triggers one re-pull.

🤖 Generated with Claude Code


Generated by Claude Code

scripts/_ownership.mjs (NEW) shared visibility→ownership mapping honoring
dmOnlyHidden + defaultOwnership (§1); module.json download URL v0.1.0 pin →
latest pattern (§4).
…connect suites

settings.mjs defaultOwnership default 0→2 (§1); tools/test-build-ownership.mjs
(§1/§3/§4 + pins) and tools/test-sync-manager-reconnect.mjs (debounce/flap/stop + pins).
…ESTING manual steps

note-sync._buildNoteOwnership uses the shared helper (§1); push failures surface +
queue for retry (§4). TESTING.md adds manual steps for the visibility toggles,
fail-closed path, push surfacing, and reconnect re-pull.
SyncManager re-pulls on reconnect via onStateChange → _onConnectionStateChange →
debounced _resyncAfterReconnect, so Chronicle edits during a disconnect are caught
without a world reload. Flapping collapses to a single re-pull.
…+ push surfacing

_buildOwnership wires dmOnlyHidden/defaultOwnership via the shared helper (§1),
fails CLOSED to NONE on a custom-visibility permissions error (§3), and maps
per-user grants to Foundry users via the user-mapping table (§4). Create/update
push failures surface via ui.notifications; updates queue for retry (§4).
@keyxmakerx keyxmakerx merged commit 3e4f637 into main Jun 5, 2026
1 check passed
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