Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
- `presence/` - who is where: each device announces its open file on every space channel it holds, sealed with that space's key (`people.ts`, `channel.ts`, `announcement.ts`); `here.ts` tracks the active file and idleness; `unseen.ts` the files others changed that this device has not opened. Keys and identity per space come from `plugin/space-access.ts`, shared with live editing
- `spaces/` - space records (`settings.spaces`, one object each under `spaces/` in the vault storage), the partition (`spacesOf`, `mountError`), the owner's share (`owner.ts`), invites (`invite.ts`), how a device reaches a share (`access.ts`). No network and no settings: the relay broker client is `storage/adapters/share-broker.ts`, the flows are `ui/shares/share-action.ts` and `ui/shares/invite-action.ts`. `sync/` never imports it; `core/session-factory.ts` opens a share's session from its record
- `live/` - live documents: `session/` (one file's Y.Doc in its space's room: the outbox, the room log, who else is in it, the rotation it asked for), `workspace/` (which open notes are live: `sessions.ts` and each note's `noteState`, `room.ts`, `space.ts`, the join patience, renames, and `editors.ts` pairing each model with the view that edits it), `cold/` (the file sync's `LiveNotes` port, declared in `sync/live-notes.ts` so `sync/` never imports `live/`, and the agreed texts), `doc-id.ts` (a room's id), `doc-types.ts` (what goes live, in which view), and a model per kind (`text/`, `drawing/`, each with the binding that attaches its view)
- `links/` - share links, see [share links](share-links.md): the record in `settings.links`, written only through `SharedLinks` (`plugin.sharedLinks`, subscribable), `note-links.ts` (a note's live links and whether the note changed since), the snapshot (Obsidian's renderer into a hidden attached element, then `sanitize.ts`, `math.ts` and `images.ts`), `publish.ts` (seal with `@mdsync/protocol`'s link format, store through `storage/adapters/link-broker.ts`). No UI: the flows are `ui/links/`. The page a link opens is `packages/viewer`, served by the relay
- `drawing/` - Excalidraw files without Obsidian: reading the scene, its fingerprint, the merge by element. Pure; used by `sync/`, `vault/` and `live/`
- `storage/` - remote backends behind StorageAdapter, plus the registry
- `vault/` - Obsidian filesystem access, scanning, ignore rules
- `settings/` - settings model, transfer, and the settings tab sections
- `ui/` - views and modals at the top; `live/` (note header in `header/`, relay and live status in `status/`, the states both show in `live-state.ts`), `shares/` (share window, share and invite flows), `explorer/` (file tree indicators and presence), `actions/` (menu actions), `common/` (notices, icon buttons, avatars, relay text and fixes, menus, helpers: whatever two of these use)
- `ui/` - views and modals at the top; `live/` (note header in `header/`, relay and live status in `status/`, the states both show in `live-state.ts`), `shares/` (share window, share and invite flows), `links/` (share-link window, its list, the menu entries, the note header button), `explorer/` (file tree indicators and presence), `actions/` (menu actions), `common/` (notices, icon buttons, avatars, relay text and fixes, menus, helpers: whatever two of these use)
- `editor/` - CodeMirror gutter signs, the read-only share lock
- `shared/`, `utils/` - app-aware helpers vs. generic algorithms
- `styles/` - the stylesheet, one slice per feature (see Styles)
Expand Down
1 change: 1 addition & 0 deletions .claude/commands-and-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- **The share's window** (**Manage sharing** in the folder menu of a share root or anything inside it, its badge in the file explorer, **Manage** in the settings, command `manage-shared-folder` for the open file's share) holds everything about one share: one list of the people (owner with the relay: everyone with access, else whoever is here): role, where they are now, open their note, **Revoke** (owner, with the relay), the relay status, the invite form, **Pause** (Off / On this device / On all my devices), **Stop sharing** / **Leave**.
- **Invite** (the share's window, owner, needs the relay) registers the share's storage with the relay's broker, issues a token for one person and shows an `obsidian://mdsync-share` link with a generated password to send separately. Opening the link, or pasting it in **Accept an invite** (settings) or the command `accept-shared-folder-invite` (Obsidian opens a link in the vault last used, not always the right one), asks for the password (no vault storage needed: such a guest device syncs only its shares) and a new or empty folder (default `Shared/<name>`), then pulls the share there. The participant never holds storage credentials. A **Read-only** invite is pulled but never auto-pushed; the broker refuses a manual push. Its notes open in source mode follow the others' typing live, locked. Inviting the same name again (any case) replaces that person's link.
- **Shared folders** (settings, Sync tab) starts with **Use shared folders on this device** (device-local, on by default): off pauses every share here, arriving ones too, cancels running operations, and hides the rest of the section, the folder menu entries, **Manage sharing** and the Changes tab's paused line; the relay carries only the vault, and opening an invite first asks to turn shares on. The paused badge stays on a share folder that is on disk. On, it has **Your name** once the person owns an open share (how participants see them; empty shows "Owner"; stored on their shares, so it reaches all their devices), and lists this person's open shares, each with **Manage**, so a share whose folder is gone here can still be closed. **Stop sharing** (owner; ends every token at the broker) or **Leave** (participant) closes the record on all of the person's devices; the folder's files stay and sync with the vault again. **Pause** On this device stops syncing the share here, history included; On all my devices puts it on the record, so every device of the person stops at its next refresh, and Off from any of them clears it. The folder stays out of the vault meanwhile, and the Changes tab names it. **Pause shared folders added on other devices** (device-local) pauses a share new to this device in the same save that brings its record, before any pull. **Push shared folders right away** (on by default) pushes a change in a share 2 s after the last file event there (`quietQueue` in `sync/scheduler.ts`), comparing and pushing only that share (`autoPushShares`). Events find their share by the records from launch; before the first refresh, the push refreshes first. Under it **Push only added, moved and deleted files** (on by default) leaves edits for a push, so their gutter marks and the Changes tab show them; open notes still update live. Read-only and paused shares never push, and with it off share paths go to the vault's **Auto-push after change** queue. A push that published nothing signals no one. After a refresh in which an owned share compared fine, the device sends its current S3 credentials to the broker again when they changed, so rotated keys reach participants without a new invite. Renaming a shared folder (or a folder above it) in Obsidian moves it on the person's other devices too, each at its next refresh; where the new path is taken the share keeps syncing at the old one, with a notice. A move into another shared folder or a hidden one is renamed back.
- **Share link** (file menu of a Markdown note, not Excalidraw; disabled with "needs a relay" without one) and `share-note-link` open the window that publishes the open note as a link: expiry, view limit, optional passphrase, images, the note's name. The window shows what the note's copy leaves out; the passphrase is shown once, beside the link, and never stored. **Share links of this note (n)** (file menu, when it has links), `manage-note-links` (named "Manage share links") and **Settings → Sync → Share links** open the list: views left, **Copy link**, **Update** (same link, the note as it is now; views spent stay spent) and **Stop sharing**. A shared note shows a globe badge in the file tree and a header button that open its links, with a dot when the note changed since it was shared; `update-note-links` ("Update share links of this note") updates them. `settings.links` is device-local and outside device transfer. See [share links](share-links.md).
- `refresh` and the Changes tab's refresh button also list the disk, as the first refresh after launch does: Obsidian's watcher misses files copied in bulk from outside the app. Files its index lacks sync anyway, with a notice to restart Obsidian to see them.
- `rebuild-live-note` is available only for the active note bound to a live room. It moves the room into its next generation from the current text, shedding the edit history; attribution survives.
- Presence: each open note's header shows whether it is live (and why not) and who else has it open, as coloured rings with their initial; tapping it lists them with **follow**: the tab opens each note they open and keeps their cursor in its middle; input there asks **Stop following** in a notice, and it ends on that, on a note opened there by hand, on their leaving the space, or on tapping them again. A note outside shares shows the mark only with another of your devices in it or relay trouble. The file explorer marks share roots by kind with a headcount of people in its notes, and people on the notes they have open (a collapsed folder gathers them); the desktop status bar shows the relay state and who is in shared notes. The header menu also has **Show who typed what** (shared notes), **Reconnect** (relay down; restarts only that space's socket) or **Open relay settings** (the relay refused this device), **Manage sharing** and **Rebuild live note** (not for a read-only person, who never writes the room). **Real-time sync** switches off presence and live editing together; **Show my open note to others** off keeps the device online but hides its note; **File and folder indicators** off keeps the share badge, the tree's way into the share window. Relay state everywhere comes from `realtime.hub.statusOf`. Commands `show-live-status` (named "Show relay status": the status bar item's menu: a relay row per space, who is in shared notes; the only live status on a phone) and `show-note-live-menu` (the note header's menu, **follow** included) reach both without a pointer. On a phone, tapping a people badge or the new dot in the tree shows its tooltip as a notice.
Expand Down
1 change: 1 addition & 0 deletions .claude/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Follow Obsidian's Developer Policies and Plugin Guidelines:
- Clearly disclose any external services used, data sent, and risks. Features requiring cloud services need explicit opt-in.
- No deceptive patterns, ads, or spammy notifications.
- Shared folders: `data.json` holds each share's key and a participant's broker token in plain text, like the storage credentials; the vault key also opens them through the space records. To presign for participants the relay keeps the owner's S3 credentials (its KV), so its operator is trusted with them. A participant with write access can delete the share's objects; history, not revocation, repairs that.
- Share links: creating one sends a rendered copy of one note, sealed on the device, to the user's own relay (disclosed in README). `data.json` keeps each link whole, its key included, so the owner can copy it again; the passphrase is never stored. A reader who ticks **Remember on this browser** keeps the derived non-extractable key and gate in that browser's IndexedDB until the link expires or is gone, never the passphrase. The relay holds ciphertext, a SHA-256 of the link's gate (derived from the key in the fragment, and the passphrase if any: the id alone serves and updates nothing), the view limit and expiry. Everything published passes `links/sanitize.ts` (no properties, comments, embedded notes or paths) and the viewer's DOMPurify; the viewer page runs under a CSP with no inline script, no `unsafe-eval` and no framing, and the key stays in the URL fragment, which in-page jumps must not replace. The relay's operator serves the viewer script, so they are trusted as the owner.
- Register and clean up all DOM, app, and interval listeners with the provided `register*` helpers so the plugin unloads safely; teardown is idempotent so reload/unload leaks nothing.
Loading
Loading