Skip to content

Commit ee112a7

Browse files
committed
feat: live notes custom keys + permissions
1 parent bdcfcb0 commit ee112a7

27 files changed

Lines changed: 10859 additions & 216 deletions

File tree

‎CLAUDE.md‎

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,8 @@ that is already on it needs no permission from anybody.
579579
**Page rule permissions** are bound to paths, and to locales and sites: `read:pages`, `write:pages`,
580580
`review:pages`, `manage:pages`, `delete:pages`, `write:styles`, `write:scripts`, `read:source`,
581581
`read:history`, `read:assets`, `write:assets`, `manage:assets`, `read:comments`, `write:comments`,
582-
`manage:comments`, `manage:navigation`, `read:glossary`, `manage:glossary` (`PAGE_PERMISSIONS` in `models/groups.ts`). A group grants them through **rules**:
582+
`manage:comments`, `manage:navigation`, `read:glossary`, `manage:glossary`, `read:notes`, `write:notes`
583+
(`PAGE_PERMISSIONS` in `models/groups.ts`). A group grants them through **rules**:
583584
each rule names some of them (`roles`) plus how it addresses pages (`match` + `path`, or tags) and
584585
what it does with them (`mode`: ALLOW / DENY / FORCEALLOW). Nothing is granted by default, and when
585586
several rules match, the most specific one wins — `helpers/pageRules.ts` documents the ordering.
@@ -617,6 +618,12 @@ Consequences worth knowing:
617618
`groups.checkLocaleAccess(actor, permission, siteId, locale)`; `checkAccess` routes them there too.
618619
`manage:glossary` in a locale implies `read:glossary` there. Neither is in the Guests group's seeded
619620
DENY rule, where it would outrank any ALLOW an administrator wrote for guests later.
621+
- **`read:notes` and `write:notes` ignore the path too**, and are asked in the site's PRIMARY locale,
622+
since a live note belongs to a site and to no locale (`liveNotes.permissionsFor`). `write:notes`
623+
implies `read:notes`. The Users group is seeded with `read:notes` only, so out of the box any account
624+
can open a shared note and only administrators can start one. The guests group may be granted
625+
`read:notes` (it is in `GUEST_ROLES`, not in the seeded DENY rule) and never `write:notes`, which
626+
`permissionsFor` refuses to a requester with no account whatever the rules say.
620627
- **A page that is not live is its editors' alone.** Live is `published`, or `scheduled` inside its
621628
window (`publishStartDate` inclusive, `publishEndDate` exclusive); the dates gate nothing on a
622629
`published` page, where `publishStartDate` is what a blog post is dated by. Any other page is seen —
@@ -1332,26 +1339,42 @@ menu. **`dev/specs/glossary.md` is the design**; `models/glossary.ts`, `api/glos
13321339

13331340
### Live notes
13341341

1335-
A markdown scratch document written together in real time at `/n/<key>` (HedgeDoc-style), by anybody
1336-
signed in who holds the URL. `models/liveNotes.ts`, `api/liveNotes.ts`, the note room in
1342+
A markdown scratch document written together in real time at `/n/<key>` (HedgeDoc-style), by whoever
1343+
holds the URL and may use notes on the site. `models/liveNotes.ts`, `api/liveNotes.ts`, the note room in
13371344
`core/collab.ts`; on the frontend `pages/LiveNote.vue`, `stores/liveNote.js`, and `EditorMarkdown.vue`
1338-
under its `noteKey` prop. The site switch is `features.liveNotes`, which also needs
1345+
under its `noteKey` prop. The site switch is `features.liveNotes` (on by default), which also needs
13391346
`collaborativeEditing`.
13401347

1341-
- **Not a page.** No tree entry, no page rules, no render, no save. The 24-character key (~143 bits,
1342-
`crypto`) is the credential, so it never goes in a log or an audit entry (record `noteId`), nor in
1348+
- **Two permissions bound everything else**: `read:notes` to open a note and follow it live, and
1349+
`write:notes` to create one, write in one, and own one (see [Permissions](#permissions)). The
1350+
frontend learns them from `GET /sites/:siteId/notes/_access` (`liveNoteStore.ensureAccess`) — `_`
1351+
because `access` is a key somebody could pick. A guest holding `read:notes` reads through the same
1352+
routes and joins the room as a reader, with no awareness state of its own (every guest shares one
1353+
placeholder id); a guest is never handed the Editor, Publish to Page, or an image upload.
1354+
1355+
- **Not a page.** No tree entry, no path for a page rule to match, no render, no save. The key is the credential: 24
1356+
characters from `crypto` (~143 bits) for a note made at `/n`, or whatever somebody typed — visiting
1357+
`/n/<key>` for a key no note on the site has yet creates one under it (1–64 of `[A-Za-z0-9-]`,
1358+
case-sensitive, unique per site; `KEY_PATTERN`, mirrored as `NOTE_KEY_PATTERN` in
1359+
`stores/liveNote.js`). Only a navigation creates, and only for somebody holding `write:notes` — anybody
1360+
else is told there is no such note: a note deleted under its readers reloads as gone.
1361+
Either way it never goes in a log or an audit entry (record `noteId`), nor in
13431362
any listing but its creator's own: **Profile → My Live Notes** (`GET /sites/:siteId/notes`,
13441363
`liveNotes.listByCreator`) lists only the notes the caller created, whose keys they already hold. Beyond the key, `liveNotes.rightsFor` is the only thing that decides access. Owners (the
1345-
creator, plus `manage:system`) can always do everything, and everybody else gets what `access`
1346-
allows (`editable` / `locked` / `private`). Every route and the socket ask it; nothing works it out
1347-
for itself.
1364+
creator while they hold `write:notes`, plus `manage:system`) can always do everything, and everybody
1365+
else gets what `access` allows (`editable` / `locked` / `private`) within their own `read:notes` /
1366+
`write:notes`. Every route and the socket ask it; nothing works it out for itself.
13481367
- **The collaboration room is the storage.** A note room (id `note:<noteId>`) persists its Yjs
13491368
document through `liveNotes.persist`: 2s after the last edit, at least every 10s while typing goes
13501369
on, and once more when it closes. It writes under the row lock and merges into what is stored,
13511370
because several instances may each hold the note open. `state` is the Yjs document itself, never
13521371
re-seeded from text, so a client that reconnects with offline edits merges into the same document
13531372
instead of duplicating it. `content` and `title` are derived from `state` on every write.
13541373
`titleOf` (backend) and `noteTitleOf` (`stores/liveNote.js`) must agree.
1374+
- **An untouched note is deleted after a day** (`liveNotes.purgeUntouched`, hourly off
1375+
`SYSTEM_SCHEDULE`): empty content, no revision, and `updatedAt` older than
1376+
`UNTOUCHED_NOTE_TTL_HOURS`. Only typing moves `updatedAt`, so opening a note does not keep it, and a
1377+
note written and then cleared keeps its history and is not touched.
13551378
- **Revisions** are whole copies, taken at most every `REVISION_INTERVAL_MS` (5 min) and when the last
13561379
participant leaves, and only when the content changed.
13571380
- **Deleting a note or changing its access doesn't reach open rooms by itself.** The route calls
@@ -1361,7 +1384,7 @@ under its `noteKey` prop. The site switch is `features.liveNotes`, which also ne
13611384
every storage target gets them. The type is read from the bytes (PNG, JPEG, GIF, WebP, SVG) and SVG
13621385
goes through `uploadScanSVG`. `groups.checkAccess` says no to every page permission under `n/` for
13631386
everybody, which is what hides it from the File Manager and every API. `/_files/` serves it to
1364-
anybody signed in instead, and the tree refuses `n` as a new root name. `liveNoteImages` lists each
1387+
anybody holding `read:notes` on the site instead, guests included, and the tree refuses `n` as a new root name. `liveNoteImages` lists each
13651388
note's images, and `liveNotes.delete` removes them with the note. The images are kept until then,
13661389
even after the text stops showing them, because a revision may still use them.
13671390
- **Publish to Page** (`publishToPage` in `LiveNote.vue`) makes a page of a note in the browser, with

0 commit comments

Comments
 (0)