Skip to content

Security fix: keep GM-only page text out of players' journal view - #141

Merged
keyxmakerx merged 2 commits into
mainfrom
claude/project-thread-rz71xm
Oct 3, 2026
Merged

keyxmakerx merged 2 commits into
mainfrom
claude/project-thread-rz71xm

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Requested by Key Maker · project thread

Fixes keyxmakerx/Cordinator#217
Security implication: security fix. GM-only text and pictures in Chronicle pages no longer show as ordinary text in Foundry journals, and GM-only field values are no longer stored in journal flags every client receives.
Consumer-verified: Chronicle marks GM-only text as <span data-secret> (internal/sanitize/sanitize.go:70, stripped for players by StripSecretsHTML at :124, regex at :114); the sync API leaves it in for Owner/Scribe keys (internal/plugins/syncapi/egress_sanitize.go:107); a pushed entry is stored as HTML through the same sanitizer (internal/plugins/entities/service.go:674-684).
Foundry compatibility: not run in a live Foundry. Uses Foundry's own <section class="secret"> markup (v12 to v14). Live check to be filed as a needs-operator issue.
Mockup: n/a (players stop seeing text they shouldn't; no new UI)

What this changes

Before: Sync reads Chronicle with the owner's key, so a page's GM-only text reached Foundry and was written into the journal page as ordinary text, readable by every player who can see the journal.

After: GM-only text and GM-only pictures go into Foundry's own GM secret blocks, which Foundry hides from players who don't own the page. Edits made in Foundry keep it GM-only: anything inside a secret block, including one the GM made, goes back to Chronicle as GM-only content.

How: a new pure helper, scripts/_gm-secrets.mjs.

  • On pull it runs before the incoming sanitizer. A paragraph or heading holding GM-only text is split around it, and the GM-only piece goes in a secret block. The pieces share a data-chronicle-part number, and spaces at the split edges become no-break spaces so Foundry's editor keeps them.
  • On push (_collectTextPages, _collectPlayerNotes) every text run inside a secret block is wrapped in its own <span data-secret>, never a span inside a span, because Chronicle's stripper ends a secret at the first </span>. Pictures inside a secret block are marked ce-img--gm, and split paragraphs are joined again.
  • _splitByHeadings ignores headings inside secret blocks, and a heading that starts with GM-only text keeps that start on its own page. A secret never becomes a page name, and no extra "Overview" heading is pushed.

It also stops writing flags.chronicle-sync.fields, the entity's field values including GM-only ones, and removes the copy older versions stored. Nothing read that flag.

Why

Chronicle hides GM-only text from everyone below Scribe. The module has to keep it hidden on the Foundry side too, and keep it marked through a round trip, because a plain strip on pull would delete the secrets in Chronicle on the GM's next Foundry edit.

Known limits, recorded in the private issue rather than here:

  • A Chronicle "edit" grant makes those players Foundry owners of the page, and owners can see secret blocks.
  • Foundry still sends page text to every client.
  • If Foundry's editor drops the part numbers, a split paragraph comes back as separate paragraphs. It stays GM-only.

Order with #138 (pictures): on push that PR's picture pass should run first and this one second. Whichever merges second has a small conflict at the same call sites.

Test plan

  • node --test tools/test-*.mjs: 1070 pass, 0 fail. The new tools/test-gm-secrets.mjs (33 tests) covers what players can read after a pull, round trips with no change to Chronicle, secret blocks the GM made in Foundry, a stray </span>, pictures, the heading/page-name cases, and the wiring on every pull and push path.
  • CHRONICLE_DIR=../Chronicle bench/run.sh journals: 13 pass, 0 fail, against Chronicle main. The new scenario gm-secrets checks that GM-only text arrives only inside a secret block, that a GM edit in Foundry keeps the data-secret span in Chronicle, that a later Chronicle edit still arrives hidden, and that no field values are stored on the journal.
  • Manual verification in Foundry (needs-operator issue to follow)
  • CI passes

Tenet self-check

  • T-B1 security: API trust boundary reviewed. GM-only content from the owner key is kept out of players' view.
  • T-B2 plugin isolation: module-only change
  • T-B3 production UI: n/a
  • T-B4 dual-audience docs: .ai.md file index and the CLAUDE.md conventions line updated

Generated by Claude Code

Sync reads Chronicle with the owner's key, so page text arrives with its
GM-only parts. They were written into Foundry journal pages as ordinary
text that players could read. Pulls now put GM-only text and pictures
into Foundry secret blocks, and pushes turn everything inside a secret
block back into GM-only content, so an edit in Foundry keeps it hidden in
Chronicle too. Headings that start GM-only no longer become page names.

Field values (including GM-only fields) are no longer stored in journal
flags, which every client receives; nothing read them.

Fixes keyxmakerx/Cordinator#217

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Abw8XBoqPMgbdjkmkuhCN2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Abw8XBoqPMgbdjkmkuhCN2
@keyxmakerx
keyxmakerx marked this pull request as ready for review October 3, 2026 16:56
@keyxmakerx
keyxmakerx merged commit 3a4b7f6 into main Oct 3, 2026
3 checks passed
@keyxmakerx
keyxmakerx deleted the claude/project-thread-rz71xm branch October 3, 2026 17:05
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.

2 participants