spaces: a reading copy you can hand to someone who will not edit it - #435
Open
nyblnet wants to merge 1 commit into
Open
spaces: a reading copy you can hand to someone who will not edit it#435nyblnet wants to merge 1 commit into
nyblnet wants to merge 1 commit into
Conversation
"Save a reading copy…" (share popover) writes a sealed second file that opens
as a document rather than as an editor, and the eye toggle becomes the same
view for the file you are writing in.
TIERS. No new one. The format already carries both published shapes and they
were settled before this: `doc.readonly` is the SEALED copy (no session) and
`collab.role:'reader'` is the LIVE view-only copy. `readonly` had been declared
and read by almost nothing — it locked the store and left every editing control
on screen, inert, which reads as a broken editor. It opens the reader now.
WHAT THE COPY LEAVES BEHIND, in three honest grades:
CRYPTOGRAPHIC — `doc.collab` is deleted outright (share.ts's own list, via
stripCollabSecrets with no keepRoom), so no room, no symmetric read key and
no private signing key of any kind travels. The recipient cannot read the
room, write to it, or join it, because the material a socket must present
is not in the file. An invite keeps room+key because it is MEANT to follow;
a sealed copy follows nothing.
FORMAT-LEVEL — every comment thread is gone from the bytes, page-level and
block-level, replies included.
COSMETIC — `doc.readonly` itself, and the chrome the reader hides. The
document block is plaintext by design, so anyone can clear the flag. It is
intent, never a lock, and nothing here presents it as one.
An older build ignores the flag and opens the space editable, which is the
correct degradation: the guarantees that matter are absent bytes, and those are
absent whatever opens the file.
READING MODE. New spaces/src/reading.ts holds the transform and the reader's
Previous/Next navigation; the diffs in editor.ts, collabui.ts, share.ts,
main.ts and styles.css are deliberately small. Reading mode now removes the
tools rather than disabling them — insert, undo/redo, the properties panel and
toggle, the format bar, block gutters, the page-icon picker and the sidebar's
new-page button — and keeps the page tree, ⌘K, print and ⋯. A sealed copy also
loses the eye, the save split and the live control, and Esc cannot leave it.
Print already hides the chrome and now hides the reader's footer too.
VERIFIED. scripts/test-spaces-reading.ts asserts on the SERIALIZED document,
not on "the strip function ran" — 40 checks, following test-spaces-invite.ts.
Sabotaged four ways (collab strip, comment strip, page-comment strip, the flag)
and each turned exactly its own assertions red. Browser-measured on a built
shell over http with getBoundingClientRect at 1728px and in a 390px frame: no
horizontal overflow, both nav ends present, sealed bar down to About/Search/⋯.
Shell 277,769 -> 280,245 B against origin/main built side by side (+0.9%).
Rig registered in scripts/test-spaces.mjs and in ci.yml — the manifest check
fails on an unregistered rig, so the step could not be a follow-up.
Build size
Updated: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A space is already the whole artifact. What was missing was a file you can hand
over without also handing over the workspace — the editing tools, the
half-finished conversations in the margins, and the keys to the live room.
"Save a reading copy…" in the share popover writes that file. It opens as a
document: the pages, the tree, ⌘K search and print, and none of the machinery
for changing them. The eye toggle is the same view for the file you are writing
in, and every page now carries a Previous / Next pair.
Tiers — no new one, and that is the point
bento/slides has three. Spaces already carried two of the same shapes in its
format, settled before this branch:
doc.readonlycollab.role:'reader'share.ts readerCopy)Slides' third tier answers a question spaces does not ask (a presentation
package boots into a show). Adding a "published" field would be a second
spelling of
readonlythat old files answer only one of. So this branch adds notier — it writes the one that had a field in the format, a check in
main.ts,and no button anywhere: the one property a sender could choose was the one
they had to hand-edit the JSON to set. It also only locked the store, so a
reading copy arrived as the full editor with every control inert.
What is stripped, in three honest grades
The failure this app's history says to avoid is presenting a cosmetic guarantee
as protection, so:
doc.collabis deleted outright — throughshare.ts's ownstripCollabSecretswith nokeepRoom, so it is that list and not a secondcopy of it. No
room, no symmetrickey(the read capability for every frameand blob the relay holds), no
ownerPriv,writerPrivorinvite.priv. Therecipient cannot read the room, write to it, or join it — because the material
a socket must present to the relay is not in the file. An invite keeps
room+keybecause it is meant to follow; a sealed copy follows nothing,so it keeps nothing.
Page.commentsand
Block.comments, replies included. Not hidden: absent. The starter spaceitself says a comment is saved in the file and anyone you send it to can read
it; this is the export where that stops being the reader's problem.
doc.readonly, and every piece of chrome the reader hides.Anyone can open the HTML and flip it back — the document block is plaintext by
design. It is intent, never a lock, and nothing in this PR, the changelog or
the source describes it as one.
What I found already in
share.ts: it does this well.stripCollabSecretsis derived by deleting rather than by rebuilding from an allow-list, with the
reasoning written down, so a private field added to
CollabCredslater iscovered without anyone remembering. Its default (no
keepRoom) already dropscollaboutright, which is exactly what a reading copy needs — so the striphere is a call, not a reimplementation. No hole found;
readerCopyandinviteCopyboth keep the boundary they claim.Format additivity
Absent
readonly= old behaviour. A reading copy is always sealed, so the flagis never written
false— the transform adds exactly one top-level key andremoves exactly one, and the rig asserts both. An older build opening a reading
copy does not fail: it ignores the flag and opens the space editable. That is
the correct degradation, and it is why the guarantees that matter are removal
rather than a flag — bytes that are not in the file are not in the file whatever
opens it.
The static preview, print, and encrypted spaces
serializeAuto/writeUpdatedFileAslike every other export, so it carries thestill render — verified on the real file: host
div+ its removerscriptasthe immediate next sibling, 5,222 B, and no comment text in it (the renderer
has never heard of comments).
@media printblock already hides the bar, the panels and the gutters. Thefooter is the one thing reading mode adds, so it is the one thing print has
to take away again — a "Next ›" card is a control.
copy of a locked space is written encrypted with the same password, so its
reader meets the password gate before the reader — and
previewAllowedrefuses a still for an encrypted body, so it carries none. A plaintext home
page beside the ciphertext is the leak the password exists to prevent, and a
reading copy is the last file to make that exception for.
Verification
scripts/test-spaces-reading.ts(new) asserts on the serialized document,never on "a strip function was called" —
JSON.stringify(copy)is literally whatserializeAutowrites into#bento-doc, so a full-text scan of it is a scan ofwhat a recipient opens in a text editor. It follows
test-spaces-invite.ts,including its premise checks (the open document really does hold the owner key
and the comments) and its full-text scan for key material rather than a field
check — the indirection hole this repo has shipped twice.
Sabotage-verified, four ways, each restored afterwards:
stripCollabSecrets(out)→ no-opstripComments(out)→ no-opdelete page.comments→ no-op (block-level left intact)out.readonly = trueremovedBrowser-measured on a built shell over
http://127.0.0.1:5321, on auniquely-named copy whose marker was confirmed in the served bytes (
fetchoflocation.href, since the app overwrites<title>), withgetBoundingClientRect:36→0, properties panel 0, gutters 18→0. Kept: eye 36, search 36, ⋯ 36, save
128, sidebar 244. Reader footer appears at 726×81.
readonly:truefile, produced from the page and servedback): boots
sp-app sp-sealed sp-reading; the only visible bar buttons areAbout, Search, ⋯; save, the eye and the live control are all 0; 0
contenteditable nodes, 0 comment markers; Esc leaves it in reading mode.
scrollWidth390 =innerWidth),sidebar collapsed, both nav cards 158 px side by side, prose column 354 px.
stylesheet, not as source text.
Could not verify
Emulation.setEmulatedMediais not reachable from thetools here, so
@media printwas checked as a parsed rule rather than byrendering to paper. Note
printNow()renders its own tree viarenderPageand never includes the footer anyway — the rule covers the other path, a
browser ⌘P over the on-screen DOM.
writeUpdatedFileAs) — the embedded browser deniesfile-system-access writes, so the copy under test was produced by
serializeAuto+ POST rather than by pressing the button. The button's ownwiring (
onShareCopy→serializeAuto→writeUpdatedFileAs,keepHandlefalse) is the same path the invite and view-only copies already use.
inherited from
serializeAuto+previewAllowed, both kernel-tested; I didnot set a password and re-export.
verified present and inert, not rendered by a real thumbnailer.
Shared-surface note
scripts/test-spaces.mjsand.github/workflows/ci.ymlare touched by path.The CI step is not deferrable to a follow-up:
test-spaces.mjs --manifestfails on a rig that is listed but unregistered, so registering it is part of
adding it. Flagging for the lead, per the standing conflict on
ci.yml.