') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - Infinity6542/Operation-Vault · GitHub
Skip to content

Latest commit

History

274 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

aliases
tags
datecreated2025-12-24T02:07
datemodified2025-12-30T21:44

Operation Vault

Operation Vault (OV, OpVault) consists of three major components: an Obsidian client, web client and relay server. Both the web client and relay server are meant to be self-hostable, while the Obsidian will work after installation. The plugin must work in conjunction with the relay server.

The objective is to transform an Obsidian vault into something that has similar “cloud” capabilities to Google Docs. This means quick generation of links with minimal setup required on both ends of a connection. Oh, and did I mention that this will be (mostly) peer to peer? And end-to-end encrypted?

Capabilities

Multiplayer

OpVault will utilise a relayed P2P (peer-to-peer) system. Below is a good diagram of what’s happening:

sequenceDiagram
par P1 Connecting
Peer 1->>Server: Connects to server
Peer 1->>Server: Joins master channel
and P2 Connecting
Peer 2->>Server: Connects to server
Peer 2->>Server: Joins master channel
end
Peer 1->>Server: Creates UUID for file and joins channel
Peer 1->>Server: A .yjs and manifest.json file is sent for upload
Peer 2->>Server: Joins channel with UUID
Server->>Peer 1: Notification of Peer 2 joining (updated list)
Peer 2->>Server: Requests file either from peer/cloud
Server->>Peer 2: Manifest, Yjs and file are sent back
par Updates P1->P2
Peer 1->>Server: sync_update and awareness packets are sent
Server->>Peer 2: Updates to file and awareness relayed
and Updates P2->P1
Peer 2->>Server: sync_update and awareness packets are sent
Server->>Peer 1: Updates to file and awareness relayed
end
Loading

Of course, it’s a lot more complicated than this. Hopefully there’ll be documentation at some stage as to what “processing” actually means, but the server also observes which clients it should forward the data to rather than blindly firing data at every device connected to it.

Anyway, multiplayer functionality will have these features:

  • Live cursors (yay!)
  • Native Obsidian collaboration
  • Frictionless collaboration
    • No account registration required (yippee!)
    • Begin sharing in 1 click (generate link)
    • Begin collaborating in 2 clicks (click link + confirm nickname)
  • Encrypted data both in trasit and at rest (cloud)
  • Fast updates with minimal data transfer
  • Simple web UI for non-Obsidian users
    • Comes with KaTeX, Excalidraw, Canvas, etc. rendering support!

Want more features? Open a feature request as an issue and I’ll take a look into it!

Hosting

That’s right—I’ll also be supporting hosting using this plugin! Now, don’t get me wrong, [[https://github.com/oleeskild/obsidian-digital-garden|Digital Garden]] is absolutely awesome—I even use it regularly. However, I do think that it’s perhaps too difficult to set up for the average user, not to mention I want to fulfil my (perhaps far-fetched) dream of powering multiple sites from a single Obsidian vault.

Anyhow, I plan on implementing these features as part of the hosting functionality:

  • Easy and simple site management
    • Good integration w/ existing sites
    • Easy deployment of new sites
  • Private/Public toggle
  • Simple web UI
    • Comes with KaTeX, Excalidraw, Canvas, etc. rendering support!
  • “Freezing” blocks of files
  • Regex-powered redaction
    • Hopefully behind a good UI. I don’t think I’ve met a single person that understands how to use regex

Stack

This is a pretty large stack, so bear with me for a sec…

Plugin

This component is responsible for working as both a host and a guest switching the two. It should be able to transmit/receive updates, including cursor positions as well as file deltas. It should be able to calculate deltas when transmitting and apply them when receiving. If the plugin is acting as a host, it should also be checking for any redaction or freezing rules that should be applied.

  • TypeScript
    • Yjs (CRDT things) and fast-diff
    • CodeMirror integration

Web Client

This component will be used by people who don’t have Obsidian or are viewing a hosted version.

  • HTML, CSS, JS
    • Marked.JS
    • KaTeX
    • DOMPurify
    • Yjs
  • WebTransport and RESTful APIs

Backend

This component will handle connections between users, forwarding updates and authentication to ensure that snapshots are proper. It’ll also be the endpoint for hosting.

  • Golang
    • QUIC
    • WebTransport
    • S3
      • more

[!info] Hosting I plan on making this 100% self-hostable. The default config will assume you’re using Dokploy (it really does make it easier). I’ll also provide some configs if you don’t have Dokploy, but there’ll be more work with the reverse proxy.

Networking & Security

Data sent will be encrypted, effectively making the coordination server blind. A key pair exchange system will be used to handle encryption.

  • WebTransport (QUIC)
  • BYOS (any S3-compatible storage)
    • Default will be using Cloudflare R2
  • AES-GCM

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages