Skip to content

Enforce public identity boundary for presence - #286

Merged
spencerc99 merged 13 commits into
mainfrom
cx/fix-wikipedia-presence-limit
Jul 10, 2026
Merged

Enforce public identity boundary for presence#286
spencerc99 merged 13 commits into
mainfrom
cx/fix-wikipedia-presence-limit

Conversation

@spencerc99

@spencerc99 spencerc99 commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Store extension identity as { public, privateKey } and keep discovered sites in separate profile storage.
  • Expose public identity through explicit background messages; content initialization and EventBuffer no longer read the combined private storage record.
  • Centralize public PlayerIdentity projection in @playhtml/common, with field allowlists, bounded strings, and bounded color palettes.
  • Remove the generic 4 KiB presence-value limit and the 8 KiB raw-message limit.
  • Keep a 1 MiB pre-parse server abuse ceiling, then use Cloudflare's actual hibernation attachment write as the cumulative connection-state boundary.
  • Persist candidate connection state before mutating broadcast state, restoring the prior attachment if the platform rejects it.
  • Snapshot validated client presence state so rejected or caller-mutated values cannot be replayed on socket open or reconnect.
  • Consolidate extension identity/color storage ownership and remove stale participant/color helper paths.

Root cause

The extension stored public identity, private signing material, and browsing-derived profile fields in one flat playerIdentity object. Page-facing code could receive or load that full object before selecting public fields, producing oversized Wikipedia presence payloads and crossing the intended private/public boundary.

The final transport audit also found that join() and update() cached caller-owned values before validation. A rejected value could remain queued and throw again during flushCurrentState(), while an accepted object could be mutated after validation.

Security boundary

  • playerIdentity.public is the only identity shape returned by identity/profile messages, DOM identity events, and presence.
  • playerIdentity.privateKey remains extension-local and is not returned by those APIs.
  • Content-script identity consumers request public identity from the background instead of reading the combined storage record.
  • Discovered sites remain separate profile data and are not included in public identity or presence.
  • Presence join and update paths reject fields outside the public identity schema.

Moving signing material from content-script-readable browser.storage.local into a background-owned non-extractable key store remains part of the signing design in #264. This PR fixes accidental data flow without redesigning signing.

Presence limits

Shared presence validation now checks JSON serialization and known-channel schemas without imposing a generic byte threshold. PartyKit rejects raw messages above 1 MiB before parsing as an abuse safeguard.

Presence channels must survive Durable Object hibernation, so cumulative connection state is persisted through connection.setState(). The server derives and validates candidate channels, attempts the real platform attachment write, and only then updates room state. A rejected attachment restores the prior accepted state and returns a presence error without broadcasting the candidate.

Review

Final review covered the complete branch diff, current origin/main, runtime-message and DOM boundaries, content-script storage access, persisted identity migration, presence join/update/reconnect paths, hibernation attachment atomicity, extension profile access, docs, release notes, and project instructions.

An independent final review found the EventBuffer content-script storage read; that path was moved behind GET_PUBLIC_PLAYER_IDENTITY and re-reviewed with no remaining high-confidence findings.

Validation

  • Extension: 53 files, 333 tests passed
  • PlayHTML: 34 files, 355 tests passed
  • Common presence protocol: 19 tests passed
  • PartyKit presence policy/server: 19 tests passed
  • PartyKit TypeScript check passed
  • Common, PlayHTML, and extension production builds passed
  • Combined website/docs build passed
  • git diff --check passed
  • bun run lint still has the previously documented unrelated extension visualization, website shared, worker Cloudflare-global, and WXT manifest type errors

Residual risk: Cloudflare attachment rejection is covered by atomic persistence/rollback unit tests but was not forced in a live Durable Object runtime.

@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

bun add https://pkg.pr.new/@playhtml/common@286
bun add https://pkg.pr.new/playhtml@286
bun add https://pkg.pr.new/@playhtml/react@286

commit: 6e284a8

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying playhtml with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4949208
Status: ✅  Deploy successful!
Preview URL: https://b06d38b0.playhtml.pages.dev
Branch Preview URL: https://cx-fix-wikipedia-presence-li.playhtml.pages.dev

View logs

@spencerc99
spencerc99 marked this pull request as ready for review July 8, 2026 05:19
Comment thread extension/src/utils/presenceIdentity.ts Outdated
Comment thread extension/src/storage/playerIdentity.ts Outdated
Comment thread packages/common/src/cursor-types.ts
Comment thread packages/playhtml/src/index.ts Outdated
@spencerc99

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and project-instruction compliance.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploying we-were-online-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 33c8081
Status: ✅  Deploy successful!
Preview URL: https://ff9286c5.we-were-online-website.pages.dev
Branch Preview URL: https://cx-fix-wikipedia-presence-li.we-were-online-website.pages.dev

View logs

Comment thread extension/src/storage/EventBuffer.ts Outdated
Comment thread partykit/presenceMessage.ts Outdated
…ence-limit

# Conflicts:
#	extension/src/__tests__/EventBuffer.test.ts
#	extension/src/__tests__/collectors.integration.test.ts
#	extension/src/__tests__/participant.test.ts
#	extension/src/entrypoints/background.ts
#	extension/src/storage/EventBuffer.ts
#	extension/src/storage/participant.ts
@spencerc99
spencerc99 merged commit dca1704 into main Jul 10, 2026
18 checks passed
@spencerc99
spencerc99 deleted the cx/fix-wikipedia-presence-limit branch July 10, 2026 17:48
spencerc99 added a commit that referenced this pull request Jul 25, 2026
Brings the branch up to date with #284 (users API), #329, and #327
(dead-code deletions). Key resolutions:

- index.ts: kept the refcounted presence-transport registry; dropped the
  branch's copies of config vars deleted by #327 (config now derives from
  configuredOptions); element awareness identity now resolves through
  resolveMyIdentity()/usersAPI instead of cursorClient/generatePersistent.
- Extension identity injection: usersAPI.adoptIdentity() (main) followed by
  elementAwarenessClient.refreshIdentity() (branch) so the element transport
  re-joins with the injected identity; listener installs unconditionally.
- Restored MAX_PRESENCE_VALUE_BYTES to common/presence-protocol.ts: removed
  on main by #286 as unused, but this branch's element-awareness client
  bounds its channel payloads with it.
- Element-awareness tests: kept the branch's transport-era suites, adapted
  handler access to the elementHandlers named export (main removed it from
  the public singleton).

Verified: playhtml 417/417, common 16/16, react 50+1 integration all pass;
tsc clean for packages; lint failures limited to pre-existing extension
errors also present on main.
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