diff --git a/LEGAL.md b/LEGAL.md new file mode 100644 index 0000000..2fd3a0a --- /dev/null +++ b/LEGAL.md @@ -0,0 +1,45 @@ +# Legal Notice + +Ignis is not affiliated with, endorsed by, or associated with Dynalist Inc. or Obsidian. + +Ignis is an independently developed interoperability tool. It contains no Obsidian source code, binaries, or assets. No part of Obsidian is distributed, bundled, or included in this repository. Ignis serves its own HTML page that loads the shim layer, then dynamically loads Obsidian's unmodified scripts. Obsidian's own files are never altered, patched, or transformed, either on disk or in transit. + +Ignis works by providing a compatibility layer that implements browser-compatible equivalents of the Node.js and Electron APIs that Obsidian depends on. The user must obtain their own licensed copy of Obsidian separately. Ignis has no standalone functionality without it. + +## Interoperability under EU law + +The development of Ignis involved studying Obsidian's module interface layer to understand how it interacts with the Electron and Node.js runtime. This work falls under the interoperability provisions of [Directive 2009/24/EC of the European Parliament and of the Council](https://eur-lex.europa.eu/eli/dir/2009/24/oj/eng) (the EU Software Directive), which permits decompilation and analysis of a computer program to achieve interoperability with an independently created program. + +Specifically: + +- **Article 6(1)** permits reproduction and translation of code where it is indispensable to obtain the information necessary to achieve interoperability of an independently created program with other programs, provided that: (a) the acts are performed by a person having a right to use the program, (b) the interoperability information was not previously readily available, and (c) the acts are confined to the parts necessary to achieve interoperability. +- **Article 5(3)** permits a lawful user to observe, study, and test the functioning of a program to determine the ideas and principles underlying its elements, including its interfaces. +- **Article 8** states that any contractual provisions contrary to Article 6 or the exceptions in Article 5(2) and (3) shall be null and void. + +The shim layer targets the runtime interface boundary, the points where Obsidian calls Node.js and Electron APIs, and replaces them with browser-compatible equivalents backed by a server. No Obsidian application logic, algorithms, or non-interface code is reproduced. Ignis also includes a plugin that uses Obsidian's plugin API to add browser-specific functionality such as file upload and download. This plugin interacts with Obsidian in the same manner as any third-party community plugin. + +## What Ignis does and does not do + +**Does:** +- Provide independently written JavaScript modules that implement Node.js and Electron API surfaces in a browser context +- Provide a server that exposes filesystem operations over HTTP and WebSocket +- Load a shim layer at runtime that intercepts Obsidian's API calls before they reach the (absent) Node.js and Electron environment + +**Does not:** +- Distribute, bundle, or include any Obsidian source code, binaries, or assets in this repository. Obsidian is downloaded by the user's own container instance directly from official sources at runtime. +- Modify, patch, or alter any of Obsidian's files on disk +- Reproduce Obsidian's application logic, algorithms, or non-interface code +- Function as a standalone application without Obsidian +- Compete with or replace Obsidian + +## Regarding Obsidian's Terms of Service + +Obsidian's Terms of Service (Section: Restrictions, item iii) restrict reverse engineering except for the purpose of developing third-party plugins for non-commercial use. To the extent that this restriction conflicts with the rights granted under the EU Software Directive, Article 8 of the Directive renders such contractual provisions null and void. + +This project is developed and maintained by an individual based in the European Union, where the Directive applies as implemented in national law. + +## Good faith + +This project exists because its author uses Obsidian daily and wants to access it from a browser. It is shared in the belief that tools enabling software interoperability benefit users and are protected under EU law. There is no intent to harm Obsidian, Dynalist Inc., or their business. If you are a representative of Dynalist Inc. and wish to discuss this project, please reach out via the contact information provided below. + +Email: ignis@thiefling.com diff --git a/README.md b/README.md index 610ee4b..de628cc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@

Run Obsidian in the browser. No remote desktop required.

+ +

+ Try the live demo +

## What is this @@ -20,35 +24,70 @@ While Obsidian's local-first approach works well for most users, options for acc ## Project Status -Ignis is **experimental**. Core functionality works, and some browser specific enhancements have been added, like file upload and download. Plugin support is an ongoing process of trying out plugins and finding what gaps in the shim still need to be plugged, but if a plugin uses primarily Obsidian's plugin API chances are it will work just fine. +What started as an experiment turned out to be more viable than expected, and the project has grown into a usable browser-based client with multi-vault support, file upload and download, workspaces opened across browser tabs, and live sync between tabs. I now use it as my everyday Obsidian instance and intend to maintain it for the foreseeable future. + +Plugin compatibility depends on what APIs a plugin uses; most plugins built on Obsidian's plugin API work, anything requiring Node native modules or `child_process` doesn't. See [What doesn't work](#what-doesnt-work) for the full list of known limitations. ## What works -- Creating, opening, and switching between multiple vaults -- Editing notes (markdown, canvas, bases, all core editor features) -- Community plugins to some degree (anything that doesn't need native Node modules, hopefully). -- File upload and download from the browser -- Live sync of external file changes via WebSocket -- Obsidian Sync has been tested and seems to be working fine, as long as the tab remains open obviously. -- Obsidian Headless has been integrated and can be used for continous synchronization. Can't be used alongside Obsidian Sycn in the browser, you can only pick one sync solution in order to avoid conflicts. +- All core editor features: markdown, canvas, bases, and the command palette. +- Context menus throughout the UI. +- Image rendering, inline image URLs, and image paste from the clipboard. +- Print to PDF, via a hidden popup iframe. +- Mobile UI auto-activates when the window is under 600 px wide. +- Themes and CSS snippets. +- Most community plugins built on Obsidian's plugin API. +- Cross-origin plugin requests via `requestUrl` and `fetch`, proxied through the server. +- Obsidian Sync, in self-hosted deployments with a logged-in browser tab open. -## Plugin Compatibility +## What doesn't work -Plugin support depends on what APIs a plugin uses. Anything built on Obsidian's plugin API generally works. Plugins that depend on Node.js modules might work depending on which are used. +- Plugins that depend on Node native modules or `child_process` won't load. +- Streaming `zlib` classes (`createGzip`, `createDeflate`, etc.) aren't implemented. The synchronous and callback variants work via `pako`. +- The synchronous file picker (`dialog.showOpenDialogSync`), used by plugins like Importer, has a staged-files workaround: the shim asks you to pick once and serves the result on retry. Usable but rough. +- `safeStorage` is passthrough by design: `isEncryptionAvailable()` returns `false` and `encrypt`/`decrypt` are no-ops. Anything plugins store via `safeStorage` ends up as plaintext on disk. A server-side encrypted option is planned but not yet implemented; until then, treat anything `safeStorage` produces the same as anything else in the vault. -Compatibility is currently tracked in [Issue #9](https://github.com/Nystik-gh/ignis/issues/9). +Compatibility for specific community plugins is tracked in [Issue #9](https://github.com/Nystik-gh/ignis/issues/9). -## Caveats +## What Ignis adds on top of default Obsidian features -_This section will be expanded as issues are documented._ +**Vaults.** +- Custom UI for Obsidian's multi-vault support, allowing create, open, switch, rename, and delete. +- Different vaults can be loaded in different browser tabs. -- Community plugins that rely on `child_process` or native Node addons will not work at the moment. -- Mobile browser support is not a priority. It works, but the UX is not great. But I have ideas. -- File picker has a workaround to deal with synchronous file selection issues. Usable, a bit hacky. +**Files.** +- File upload from the local machine via a ribbon icon, right-click on a folder -> Upload file, or drag-and-drop into the UI. +- File and folder download via right-click any note -> **Download**, or any folder -> **Download as ZIP**. + +**Multi-tab and workspaces.** +- Live file sync between browser tabs via WebSocket: open the same vault in two tabs and edits propagate within a second. +- Saved workspaces can be opened in separate browser tabs via a `?workspace=` URL parameter, so each tab can hold a different layout of the same vault. +- The bridge plugin adds an "Open workspace in tab" command to the command palette. + +**Server-side sync.** +- Obsidian Headless is implemented as a server-side plugin that performs continuous sync without needing an active browser tab. Only one of Obsidian Sync or Obsidian Headless can run per vault. + +**Server-side integration.** +- Adds a plugin system inside the server itself, separate from Obsidian's community plugin system (WIP). +- Ignis-specific settings appear as their own tabs inside Obsidian's Settings modal. +- Status bar indicators surface server state and headless sync activity. + +## Performance + +A few design decisions worth knowing about for someone evaluating Ignis against large vaults or slow storage: + +- A pre-compressed bootstrap response delivers vault info, vault list, metadata tree, and plugin list in a single call. +- Indexer pre-fetch warms the content cache so Obsidian's startup index hits cache instead of the network. +- An LRU content cache (50 MB by default) keeps memory use bounded regardless of vault size, so Ignis doesn't hold the whole vault in memory. +- Write coalescing debounces rapid writes for slow filesystems (rclone, FUSE, NFS, SMB). + +## Browser compatibility + +Tested in Chrome, Brave, and Firefox, with limited testing in Safari. ## Authentication -Ignis has **no built-in authentication**. The server is completely open by default. +Ignis has **no built-in authentication** and serves plain HTTP by default. Both authentication and TLS termination are expected to be handled by whatever you put in front of it. If you are exposing Ignis to the internet, **you should really** put an authentication layer in front of it. Options include: @@ -66,22 +105,12 @@ Example for Basic Auth, and Authelia can be found [here](examples). ## Setup with Docker Compose -Ignis is not published to a registry yet. You need to build the image locally. - -```bash -git clone https://github.com/Nystik-gh/ignis.git -cd ignis -docker compose up -d -``` - -On first start, the container will download Obsidian from the official servers and set everything up, and also install Obsidian Headless CLI. This takes a minute or two. - Example `docker-compose.yml`: ```yaml services: ignis: - build: . + image: nobbe/ignis:latest ports: - "8080:8080" environment: @@ -98,6 +127,10 @@ volumes: obsidian-app: ``` +Then `docker compose up -d`. On first start the container downloads Obsidian from the official source and installs Obsidian Headless CLI. This takes a minute or two. + +To build from source instead of pulling the image, clone the repo and replace `image: nobbe/ignis:latest` with `build: .`. + ### Volumes | Mount | Description | @@ -114,15 +147,27 @@ volumes: | `VAULT_ROOT` | Path to vault storage inside the container | `/vaults` | | `DATA_ROOT` | Path to persistent data (plugin config, sync state, auth tokens) | `/app/data` | | `OBSIDIAN_VERSION` | Obsidian version to download | `1.12.7` | +| `OBSIDIAN_ASSETS_PATH` | Where the extracted Obsidian app files live. Override if you're pointing at a pre-extracted directory instead of letting the entrypoint download. | `/app/obsidian-app` | +| `AUTO_CREATE_DEFAULT` | When `true`, creates a "My Vault" vault on startup if no vaults exist. Useful for fresh installs. | `false` | | `PUID` | User ID for file ownership | `1000` | | `PGID` | Group ID for file ownership | `1000` | | `WRITE_COALESCE_MS` | Debounce window (ms) for rapid writes. Useful for slow filesystems (rclone, NFS, SMB). Set to `0` to disable. | `5000` | -| `DEMO_MODE` | Enable demo mode (per-session vaults, auto-cleanup, proxy allowlist, login blocking). See [examples/demo/](examples/demo/). | `false` | -| `DEMO_MAX_SESSIONS` | Concurrent demo session cap. New visitors get a 503 capacity page when full. | `20` | -| `DEMO_VAULTS_PER_SESSION` | Max vaults per session (vault create returns 507 past this). | `3` | -| `DEMO_SESSION_QUOTA_BYTES` | Cumulative byte budget per session across all session vaults. | `716800` | -| `DEMO_TIMEOUT_MS` | Inactivity timeout before a demo session and its vaults are cleaned up. | `1800000` | -| `DEMO_TEMPLATE_DIR` | Directory copied into each new demo vault. | `server/demo-template/` | + +Demo mode adds its own set of env vars (per-session vaults, auto-cleanup, proxy allowlist, login blocking). See [examples/demo/](examples/demo/) if you want to run a public demo deployment. + +### Migrating an existing vault + +Each subdirectory of `/vaults` is treated as a separate vault, so dropping in an existing Obsidian vault directory will make it available in Ignis. + +### Upgrading Obsidian + +Obsidian releases can include changes that break the compatibility shim. Each Ignis release pins a known-working Obsidian version through the `OBSIDIAN_VERSION` env var, so the recommended path is to wait for an Ignis release that bumps the version, pull the new image, and restart. + +If you want to try a newer Obsidian version before Ignis updates, set `OBSIDIAN_VERSION` in your compose file. The entrypoint will download that version on next start, but there's no guarantee it'll work cleanly with the current shim. + +### Backups + +Vault data lives as ordinary files in `/vaults`. Back it up however you back up other server-side data; Ignis doesn't provide a built in backup mechanism. ## Contributing @@ -138,46 +183,8 @@ This project is licensed under the [GNU Affero General Public License v3.0](LICE ## Legal Notice -Ignis is not affiliated with, endorsed by, or associated with Dynalist Inc. or Obsidian. +Ignis is not affiliated with, endorsed by, or associated with Dynalist Inc. or Obsidian. It is an independently developed interoperability tool and contains no Obsidian source code, binaries, or assets. No part of Obsidian is distributed or included in this repository; the Docker container downloads Obsidian directly from its official source at runtime. -Ignis is an independently developed interoperability tool. It contains no Obsidian source code, binaries, or assets. No part of Obsidian is distributed, bundled, or included in this repository. Ignis serves its own HTML page that loads the shim layer, then dynamically loads Obsidian's unmodified scripts. Obsidian's own files are never altered, patched, or transformed, either on disk or in transit. +This work falls under the interoperability provisions of [Directive 2009/24/EC](https://eur-lex.europa.eu/eli/dir/2009/24/oj/eng) (the EU Software Directive), Article 6. See [LEGAL.md](LEGAL.md) for the full rationale. -Ignis works by providing a compatibility layer that implements browser-compatible equivalents of the Node.js and Electron APIs that Obsidian depends on. The user must obtain their own licensed copy of Obsidian separately. Ignis has no standalone functionality without it. - -### Interoperability under EU law - -The development of Ignis involved studying Obsidian's module interface layer to understand how it interacts with the Electron and Node.js runtime. This work falls under the interoperability provisions of [Directive 2009/24/EC of the European Parliament and of the Council](https://eur-lex.europa.eu/eli/dir/2009/24/oj/eng) (the EU Software Directive), which permits decompilation and analysis of a computer program to achieve interoperability with an independently created program. - -Specifically: - -- **Article 6(1)** permits reproduction and translation of code where it is indispensable to obtain the information necessary to achieve interoperability of an independently created program with other programs, provided that: (a) the acts are performed by a person having a right to use the program, (b) the interoperability information was not previously readily available, and (c) the acts are confined to the parts necessary to achieve interoperability. -- **Article 5(3)** permits a lawful user to observe, study, and test the functioning of a program to determine the ideas and principles underlying its elements, including its interfaces. -- **Article 8** states that any contractual provisions contrary to Article 6 or the exceptions in Article 5(2) and (3) shall be null and void. - -The shim layer targets the runtime interface boundary, the points where Obsidian calls Node.js and Electron APIs, and replaces them with browser-compatible equivalents backed by a server. No Obsidian application logic, algorithms, or non-interface code is reproduced. Ignis also includes a plugin that uses Obsidian's plugin API to add browser-specific functionality such as file upload and download. This plugin interacts with Obsidian in the same manner as any third-party community plugin. - -### What Ignis does and does not do - -**Does:** -- Provide independently written JavaScript modules that implement Node.js and Electron API surfaces in a browser context -- Provide a server that exposes filesystem operations over HTTP and WebSocket -- Load a shim layer at runtime that intercepts Obsidian's API calls before they reach the (absent) Node.js and Electron environment - -**Does not:** -- Distribute, bundle, or include any Obsidian source code, binaries, or assets in this repository. Obsidian is downloaded by the user's own container instance directly from official sources at runtime. -- Modify, patch, or alter any of Obsidian's files on disk -- Reproduce Obsidian's application logic, algorithms, or non-interface code -- Function as a standalone application without Obsidian -- Compete with or replace Obsidian - -### Regarding Obsidian's Terms of Service - -Obsidian's Terms of Service (Section: Restrictions, item iii) restrict reverse engineering except for the purpose of developing third-party plugins for non-commercial use. To the extent that this restriction conflicts with the rights granted under the EU Software Directive, Article 8 of the Directive renders such contractual provisions null and void. - -This project is developed and maintained by an individual based in the European Union, where the Directive applies as implemented in national law. - -### Good faith - -This project exists because its author uses Obsidian daily and wants to access it from a browser. It is shared in the belief that tools enabling software interoperability benefit users and are protected under EU law. There is no intent to harm Obsidian, Dynalist Inc., or their business. If you are a representative of Dynalist Inc. and wish to discuss this project, please reach out via the contact information provided below. - -Email: ignis@thiefling.com \ No newline at end of file +This project exists because its author uses Obsidian daily and wants to access it from a browser. There is no intent to harm Obsidian, Dynalist Inc., or their business. If you are a representative of Dynalist Inc. and wish to discuss this project, please reach out: ignis@thiefling.com \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6fd096c..d685e46 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -24,32 +24,41 @@ The shim layer makes Obsidian think it's running in Electron. The bridge plugin ### Loading -The server serves its own `index.html` (in `server/assets/`) rather than Obsidian's. At startup it reads Obsidian's `index.html` once to discover which scripts Obsidian expects, then embeds that list in our HTML as a JSON array. The client-side HTML loads the shim loader and UI bundle first (non-deferred), then a small inline script dynamically injects Obsidian's scripts in order. Obsidian's files are never modified, read into responses, or transformed in transit. +The server serves its own `index.html` (in `server/assets/`) rather than Obsidian's. At startup it reads Obsidian's `index.html` once to discover which scripts Obsidian expects, then embeds that list in our HTML as a JSON array. The client-side HTML loads the shim loader and UI bundle first (non-deferred), then a small inline script dynamically injects Obsidian's scripts in order. Obsidian's files are never modified on disk, or transformed in transit. -The shim loader replaces the module system and makes a blocking HTTP request to fetch the vault's directory tree into memory. The request has to be blocking because Obsidian makes synchronous filesystem calls during page load, before the event loop is running, so the cache has to already be populated. +Before injecting Obsidian's scripts, the shim loader sets `localStorage.EmulateMobile` based on viewport width (< 600px) so Obsidian boots into its mobile UI on phones and narrow windows. The loader replaces the module system, then issues a single blocking bootstrap request that returns the vault info, vault list, metadata tree, and Ignis plugin list in one pre-compressed response. The request has to be blocking because Obsidian makes synchronous filesystem calls during page load, before the event loop is running, so the cache has to already be populated. + +Immediately after the bootstrap response is applied, the client kicks off a batched pre-fetch of text file content into the ContentCache (`POST /api/fs/batch-read`). This races Obsidian's indexer so the first wave of `readFile` calls during startup indexing tend to hit the cache instead of the network. ### Modules -| Module | Implementation | -| -------------------- | --------------------------------------------------------------------------------- | -| `fs` / `original-fs` | HTTP transport + client-side metadata/content caches | -| `electron` | ipcRenderer dispatcher, webFrame stubs | -| `@electron/remote` | Partial: clipboard (browser API), shell, dialog, Menu, BrowserWindow, nativeTheme | -| `path` | path-browserify | -| `crypto` | Web Crypto (randomBytes, createHash, scrypt) | -| `url` | Browser URL API wrapper | -| `process` | Platform/version stubs | -| `utils` | Utility functions | +| Module | Implementation | +| -------------------- | ----------------------------------------------------------------------------------------------- | +| `fs` / `original-fs` | HTTP transport + client-side metadata cache + 50MB LRU content cache. Full surface. | +| `path` | path-browserify | +| `url` | Browser URL API wrapper | +| `process` | Platform/version stubs | +| `crypto` | `randomBytes`, `randomUUID`, `scrypt` use Web Crypto. `createHash` produces real digests for SHA-1/SHA-256/SHA-512/MD5 via `@noble/hashes`. | +| `electron` | `ipcRenderer` dispatcher, `webFrame` stubs, `clipboard`, `nativeImage`, `safeStorage` (passthrough, reports unavailable). | +| `@electron/remote` | Partial: `clipboard`, `shell`, `dialog` (with a sync file picker workaround), `Menu`, `BrowserWindow`, `nativeTheme`, `session`, `systemPreferences`, `screen`, `nativeImage`, `Notification`, `app`. | +| `zlib` | Sync + callback variants via pako (`deflate`, `inflate`, `gzip`, `gunzip`, raw). Streaming classes (`createGzip` etc.) throw. | +| `os` | Identity stubs (`platform()` returns `"linux"`, `hostname()` returns `"localhost"`, etc.). | +| `events` | Standard EventEmitter implementation. | +| `util` | Common helpers (`promisify`, `inherits`, type guards). | +| `child_process` | All functions throw "not available in the web version." | +| `net` | All classes/functions throw. | +| `http` / `https` | Module is importable but `request()`/`get()` emit an `error` event; `createServer` throws. Plugins should use `requestUrl` or `fetch` (the shim routes cross-origin `fetch` through the server proxy). | +| `buffer` | Aliased to the browser `Buffer` polyfill set up by the loader. | -Unknown modules return an empty proxy and log a warning. The shim exposes two console helpers, one showing everything that has been accessed and one showing what is missing. +Unknown modules return an empty proxy and log a warning. The `node:` prefix is stripped. The shim exposes two console helpers, `window.__shimLog()` (everything that has been accessed) and `window.__shimMisses()` (accessed-but-missing properties). ### Filesystem -On page load the server returns the full directory tree, which gets cached in memory with paths, sizes, and modification times. Sync filesystem calls hit the cache rather than the network. File contents are cached in an LRU cache after first read. +Two caches on the client side. The **MetadataCache** holds `{ type, size, mtime, ctime }` for every entry, populated from the bootstrap response. Sync filesystem calls (`existsSync`, `statSync`, `readdirSync`) read from it and never hit the network. The **ContentCache** is a 50 MB LRU of file bytes, populated lazily on first read and warmed by the indexer pre-fetch on cold start. Both caches are kept current by WebSocket watcher events: writes from another tab or external changes on disk invalidate or update the relevant entries within a second. -Writes go through a server-side write coalescer (`server/write-coalescer.js`) designed for slow filesystems like rclone FUSE mounts. The first write to a file goes to disk immediately. Subsequent writes within a configurable window (default 5 seconds, `WRITE_COALESCE_MS`) are buffered in memory; the timer resets on each write. After the window elapses with no new writes, the buffered data is flushed to disk. Reads for pending paths serve the buffered content so clients never see stale data. All pending writes are flushed on graceful shutdown. +Reads not satisfied by ContentCache go through the transport layer to `/api/fs/readFile`. Sync calls use synchronous XHR to keep Obsidian's pre-boot module code working. Async calls use fetch. The transport handles vault id injection, base64 encoding for binary files, and mapping HTTP error codes back to Node errno values (`ENOENT`, `EEXIST`, `ENOTDIR`). -Sync calls use synchronous XHR to ensure blocking behavior. Async calls use fetch. Everything goes through a transport layer that handles vault ID injection, base64 encoding for binary files, and mapping HTTP error codes back to Node errno values. +Writes go through a server-side write coalescer (`server/write-coalescer.js`) designed for slow filesystems like rclone FUSE mounts. The first write to a path goes to disk immediately. Subsequent writes within a configurable window (default 5 seconds, `WRITE_COALESCE_MS`) are buffered and flushed when the debounce timer fires; the timer resets on each write. Buffered writes return to the HTTP client immediately with synthetic metadata so connection-pool starvation on rapid-fire writes (e.g. `workspace.json` autosaves) doesn't stall unrelated reads. Reads for pending paths serve the buffered content so clients never see stale data. All pending writes are flushed on graceful shutdown. ### Translation registry @@ -63,31 +72,51 @@ All hooks are synchronous and registered at module load. Translation happens onc ### IPC -IPC is implemented as a synchronous dispatcher that maps channel names to handlers. +Electron's `ipcRenderer` is the renderer's channel to the main process for things only that process can do: looking up the active vault, opening a new vault window, performing cross-origin requests, printing to PDF. Ignis has no main process, so the shim is an in-process router that returns values for sync calls and fires side effects for async ones. + +Sync channels covered include `vault`, `version`, `vault-list`, `vault-open`, `vault-remove`, `file-url`, `starter`, and `help`. Each maps to a handler that returns immediately. Async channels: `request-url` is routed to the CORS proxy, `print-to-pdf` triggers a hidden popup iframe, `context-menu` replies on the next tick. The standard `on`/`once`/`removeListener` interface works as it would in Electron. + +### Cross-origin requests + +Obsidian on the desktop can make arbitrary cross-origin HTTP requests because it runs as an Electron app rather than a sandboxed browser context. In a browser tab, the same requests would be blocked by CORS or rejected by the same-origin policy. Plugin installs from GitHub, theme asset downloads, calls to third-party APIs: all of it assumes cross-origin is available. + +The shim handles this transparently. `window.fetch` and `window.requestUrl` are intercepted. Same-origin requests pass through unchanged. Cross-origin requests are POSTed to `/api/proxy`, which performs the outbound call from the server with headers that mimic Obsidian's desktop runtime: `Origin: app://obsidian.md` and the browser's own User-Agent. The response body is returned base64-encoded so binary content survives the JSON round-trip; the shim decodes it and hands the caller a normal `Response` or `requestUrl` result. + +The proxy itself is intentionally generic. It forwards method, headers, and body verbatim and returns whatever the upstream sent. In demo mode, an allowlist restricts the hostname to a known-safe set; in normal self-hosted mode there's no restriction, which is one of the reasons the server needs to be behind authentication when exposed to the internet. + +### Workspaces in browser tabs + +Obsidian's Workspaces core plugin lets you save a window layout under a name. Ignis adds a `?workspace=` URL parameter that binds a tab to a specific layout. The bridge plugin's "Open workspace in new tab" command opens the picked workspace at `?workspace=` in a fresh tab. + +The fs shim redirects reads and writes of `.obsidian/workspace.json` to a per-workspace file (`.obsidian/workspace..json`), giving each tab its own layout. It also rewrites the active field on reads of `workspaces.json` so each tab's menu shows its own workspace as active. + +Two tabs sharing a vault stay in sync through the file watcher. ### Obsidian Plugin Compatibility -Obsidian evals plugin code with its own require that checks its internal module map first, then falls back to the window-level require, which is the shim. Plugins that use the filesystem, path utilities, or crypto get the shim implementations without any changes. Plugins that need child processes or native addons won't work (for now)*. - -__child_process may be shimmable, not yet explored__ +Obsidian evals plugin code with its own require that checks its internal module map first, then falls back to the window-level require, which is the shim. Plugins that use the filesystem, path utilities, or crypto get shim implementations without any changes. Plugins that need child processes, raw sockets, or native addons will load but throw on use; the error message names the missing API. ## Vaults -Any subdirectory under the vault root is treated as a vault. The active vault is selected via a `?vault=` URL parameter. Without the queryparam, the last active vault is loaded, or the first discovered. +Any subdirectory under the vault root is treated as a vault. The active vault is selected via a `?vault=` URL parameter. Without the queryparam, the last active vault is loaded (from `localStorage.last-vault`), or the first discovered. ## Server An Express server that handles filesystem operations, vault management, static file serving, and plugin route dispatch. **Route groups:** -- `/api/fs/*` - filesystem operations (read, write, stat, tree, mkdir, etc.) -- `/api/vault/*` - vault CRUD and config -- `/api/plugins/*` - Ignis plugin management (list, enable, disable) __WIP__ -- `/api/ext/:pluginId/*` - routes registered by individual Ignis plugins +- `/api/fs/*` - filesystem operations (read, write, stat, tree, mkdir, batch-read, download, download-zip, etc.). +- `/api/vault/*` - vault CRUD and config. +- `/api/bootstrap` - one-shot cold-start endpoint; returns vault info + list + metadata tree + plugin list as a single pre-compressed response, cached per vault with mtime-based invalidation. +- `/api/proxy` - cross-origin HTTP proxy used by the fetch and requestUrl shims. +- `/api/version` - server version and git hash. +- `/api/plugins/*` - Ignis plugin management (list, enable, disable). __WIP__ +- `/api/ext/:pluginId/*` - routes registered by individual Ignis plugins. +- `/vault-files//` - static file serving rooted at a vault, used by Obsidian for image/attachment resource URLs. -**WebSocket:** A file watcher monitors vault directories and pushes change events to connected clients, keeping the client-side metadata and content caches in sync. The websocket is also used by the headless-sync plugin to report status. +**WebSocket:** A file watcher monitors vault directories and pushes change events to connected clients, keeping the client-side metadata and content caches in sync. An echo guard suppresses events caused by the same client's recent writes so they don't bounce back. The watcher also carries plugin-defined message types (e.g. headless-sync status broadcasts). -**Bridge plugin auto-install:** On server startup and vault creation, the server copies the ignis-bridge plugin into each vault's `.obsidian/plugins/` directory. +**Bridge plugin auto-install:** On server startup and on vault creation, the server copies the ignis-bridge plugin into each vault's `.obsidian/plugins/` directory. ## Plugins @@ -101,7 +130,12 @@ Standard community and core Obsidian plugins. They work through the shim layer w An Obsidian plugin auto-installed into every vault by the server. Source lives in `plugin/`, built to `plugin/main.js`. -It adds file actions to Obsidian's UI: file download, folder ZIP download, and file upload via ribbon icon and context menu. It also injects custom settings tabs into Obsidian's settings modal by monkey-patching `app.setting.onOpen`, currently providing an Ignis plugin management tab. +It contributes: +- **File actions**: a ribbon icon for uploading files into the current folder, and right-click menu items: Download (single file), Download as ZIP (folder), and Upload file (folder). +- **Commands**: `Open workspace in new tab` (with a FuzzySuggestModal listing saved workspaces). +- **Status bar item**: a dot showing the WebSocket connection state to the Ignis server. +- **Settings injection**: monkey-patches `app.setting.onOpen` to add two tabs in their own "Ignis" sidebar group. General (server status, version, GitHub link, update check against the GitHub releases API) and Core plugins (toggle the bundled Obsidian plugins of enabled Ignis plugins on/off per vault). Each enabled Ignis plugin's bundled Obsidian plugin also gets pulled into a separate "Ignis Core Plugins" sidebar group. +- **Demo guards**: in demo mode, a MutationObserver disables every email/password input that appears anywhere in the document and rewrites its placeholder. Not user-installable through Obsidian's plugin browser. Managed entirely by the server. @@ -113,6 +147,8 @@ An Ignis plugin is a Node.js package under `server/plugins//` that exports When enabled, a plugin's Express router is mounted at `/api/ext//`. A plugin can also optionally bundle an Obsidian plugin, a directory containing a standard Obsidian plugin (manifest.json, main.js) that gets auto-installed into the vault on enable and removed on disable. This bridges the server and client sides: the Ignis plugin handles server logic and routes, while the bundled Obsidian plugin provides the in-app UI or behavior. +The one Ignis plugin currently in the repo is **headless-sync** (`server/plugins/headless-sync/`). It wraps the [obsidian-headless](https://github.com/Yuri-Khomyakov/obsidian-headless) CLI (`ob`) and runs `ob sync --continuous` as a per-vault child process, optionally with `--pull-only` or `--mirror-remote`. Process state (running/stopped/error, pid, last activity, recent log lines) is broadcast over the WebSocket via a small per-vault subscription protocol. The bundled Obsidian plugin (`ignis-headless-sync`) adds a status bar item, a settings tab with start/stop/unlink controls, and a core-sync guard that hides Obsidian's own Sync setting from `core-plugins.json` reads while headless sync is active for that vault, so a different device syncing the "Active core plugins list" can't accidentally re-enable it. + ## Demo mode A separate operating mode for running Ignis as a public-facing demo. Enabled by `DEMO_MODE=true`. When off, none of the demo code runs and the server behaves normally. diff --git a/examples/demo/README.md b/examples/demo/README.md new file mode 100644 index 0000000..268f42f --- /dev/null +++ b/examples/demo/README.md @@ -0,0 +1,37 @@ +# Public demo deployment + +This example runs Ignis as a public, no-auth demo where anyone with the URL can spin up a transient vault and try the editor. The live demo at uses this configuration. + +Demo mode changes the security and lifecycle model: + +- **Per-session vaults.** Each visitor gets their own isolated set of vaults, tracked by a session cookie. Sessions don't share storage. +- **Transient files.** Vaults live on tmpfs in the example compose file, so everything is wiped on container restart and on session expiry. +- **Auto-cleanup.** Sessions expire after a period of inactivity; their vaults are removed in-process. +- **Capacity caps.** Concurrent sessions, vaults per session, and bytes per session are bounded, so a single visitor can't fill the disk and the host can't be flooded with sessions. +- **Proxy allowlist.** The CORS proxy is restricted to a known-safe domain list so the public demo can't be used as an open relay. +- **Login blocked.** Obsidian account login is blocked at both the proxy and the UI, so visitors can't accidentally enter credentials into a server they don't control. + +## Running it + +```bash +docker compose up -d +``` + +The bundled [`docker-compose.yml`](docker-compose.yml) builds Ignis from the parent directory, mounts a 20 MB tmpfs at `/vaults`, and configures the demo limits. Adjust the env vars below for your own deployment. + +## Demo environment variables + +| Variable | Description | Default | +| -------- | ----------- | ------- | +| `DEMO_MODE` | Enable demo mode (per-session vaults, auto-cleanup, proxy allowlist, login blocking). | `false` | +| `DEMO_MAX_SESSIONS` | Concurrent demo session cap. New visitors get a 503 capacity page when full. | `20` | +| `DEMO_VAULTS_PER_SESSION` | Max vaults per session (vault create returns 507 past this). | `3` | +| `DEMO_SESSION_QUOTA_BYTES` | Cumulative byte budget per session across all session vaults. | `716800` | +| `DEMO_TIMEOUT_MS` | Inactivity timeout before a demo session and its vaults are cleaned up. | `1800000` | +| `DEMO_TEMPLATE_DIR` | Directory copied into each new demo vault. | `server/demo-template/` | + +The standard Ignis env vars (`PORT`, `VAULT_ROOT`, `OBSIDIAN_VERSION`, etc.) still apply. See the [main README](../../README.md#environment-variables) for those. + +## Custom starter vault + +The bundled `server/demo-template/` is a minimal walkthrough of what Ignis is. To ship a richer starter vault for your own demo without committing it to the repo, mount a directory at `/app/demo-template` and point `DEMO_TEMPLATE_DIR` at it. The compose file has the wiring commented out.