mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
Merge pull request #15 from Nystik-gh/monorepo-refactor
Monorepo refactor
This commit is contained in:
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.vscode
|
||||
.prettierrc
|
||||
investigation
|
||||
vaults
|
||||
demo-vaults
|
||||
data
|
||||
tmp
|
||||
**/dist
|
||||
packages/bridge-plugin/main.js
|
||||
apps/ignis-server/server/plugins/*/plugin/main.js
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,6 +2,8 @@ node_modules/
|
||||
dist/
|
||||
investigation/
|
||||
vaults/
|
||||
plugin/main.js
|
||||
server/plugins/*/plugin/main.js
|
||||
packages/*/dist/
|
||||
packages/bridge-plugin/main.js
|
||||
apps/ignis-server/server/plugins/*/plugin/main.js
|
||||
demo-vaults/
|
||||
data/
|
||||
|
||||
53
Dockerfile
53
Dockerfile
@@ -1,53 +0,0 @@
|
||||
# Build shim-loader.js
|
||||
FROM node:22-slim AS build
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY build.js ./
|
||||
COPY src/ ./src/
|
||||
COPY plugin/src/ ./plugin/src/
|
||||
COPY server/plugins/ ./server/plugins/
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image. No Obsidian code included.
|
||||
# On first run, the entrypoint downloads Obsidian.
|
||||
FROM node:22-slim
|
||||
|
||||
LABEL com.thiefling.ignis.obsidian-version="1.12.7"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev --ignore-scripts
|
||||
|
||||
COPY server/ ./server/
|
||||
COPY scripts/ ./scripts/
|
||||
COPY images/ ./images/
|
||||
COPY plugin/ ./plugin/
|
||||
COPY --from=build /build/dist ./dist
|
||||
COPY --from=build /build/plugin/main.js ./plugin/main.js
|
||||
COPY --from=build /build/server/plugins/headless-sync/plugin/main.js ./server/plugins/headless-sync/plugin/main.js
|
||||
|
||||
RUN chmod +x /app/scripts/entrypoint.sh
|
||||
|
||||
ENV PORT=8080
|
||||
ENV VAULT_ROOT=/vaults
|
||||
ENV OBSIDIAN_VERSION=1.12.7
|
||||
ENV OBSIDIAN_ASSETS_PATH=/app/obsidian-app
|
||||
ENV PUID=1000
|
||||
ENV PGID=1000
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
VOLUME /vaults
|
||||
VOLUME /app/obsidian-app
|
||||
|
||||
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
||||
90
README.md
90
README.md
@@ -28,6 +28,10 @@ What started as an experiment turned out to be more viable than expected, and th
|
||||
|
||||
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.
|
||||
|
||||
## Variants
|
||||
|
||||
Ignis ships currently ships as a self-hosted server but I have plans for a desktop plugin. The server variant code and Readme with details and setup instructions lives here: [`apps/ignis-server/`](apps/ignis-server/)
|
||||
|
||||
## What works
|
||||
|
||||
- All core editor features: markdown, canvas, bases, and the command palette.
|
||||
@@ -96,90 +100,6 @@ A few design decisions worth knowing about for someone evaluating Ignis against
|
||||
|
||||
Tested in Chrome, Brave, and Firefox, with limited testing in Safari.
|
||||
|
||||
## Authentication
|
||||
|
||||
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:
|
||||
|
||||
- A reverse proxy with Basic Auth (nginx, Caddy, Traefik)
|
||||
- An SSO proxy like Authelia, Authentik, or OAuth2 Proxy
|
||||
- A VPN (Tailscale, WireGuard)
|
||||
- Cloudflare Application Tunnel
|
||||
|
||||
Example for Basic Auth, and Authelia can be found [here](examples).
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not run Ignis on a public network without auth. Anyone with the url can read and write your vault files.
|
||||
|
||||
|
||||
|
||||
## Setup with Docker Compose
|
||||
|
||||
Example `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ignis:
|
||||
image: nobbe/ignis:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- OBSIDIAN_VERSION=1.12.7
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ./vaults:/vaults
|
||||
- ./data:/app/data
|
||||
- obsidian-app:/app/obsidian-app
|
||||
restart: unless-stopped
|
||||
|
||||
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 |
|
||||
| ----- | ----------- |
|
||||
| `/vaults` | Vault storage. Each subdirectory is a vault. |
|
||||
| `/data` | state persistence for various ignis specific functionality, plugin management, headless sync config, etc |
|
||||
| `/app/obsidian-app` | Cached Obsidian assets. Persisting this avoids re-downloading on container recreate. |
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| `PORT` | Server listen port | `8080` |
|
||||
| `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 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
|
||||
|
||||
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, especially on how to report plugin compatibility issues. Check the [open issues](https://github.com/Nystik-gh/ignis/issues) for things to work on.
|
||||
@@ -198,4 +118,4 @@ Ignis is not affiliated with, endorsed by, or associated with Dynalist Inc. or O
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
77
apps/ignis-server/Dockerfile
Normal file
77
apps/ignis-server/Dockerfile
Normal file
@@ -0,0 +1,77 @@
|
||||
# Build stage.
|
||||
# Runs from the repo root as build context so workspace symlinks resolve.
|
||||
# Copies workspace package.jsons first for cache-friendly npm ci.
|
||||
FROM node:22-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY packages/services/package.json ./packages/services/
|
||||
COPY packages/shim/package.json ./packages/shim/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
COPY packages/bridge-plugin/package.json ./packages/bridge-plugin/
|
||||
COPY packages/server-core/package.json ./packages/server-core/
|
||||
COPY apps/ignis-server/package.json ./apps/ignis-server/
|
||||
|
||||
RUN npm ci --ignore-scripts
|
||||
|
||||
COPY build.js ./
|
||||
COPY packages/ ./packages/
|
||||
COPY apps/ignis-server/ ./apps/ignis-server/
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image. No Obsidian code included.
|
||||
# On first run, the entrypoint downloads Obsidian.
|
||||
FROM node:22-slim
|
||||
|
||||
LABEL com.thiefling.ignis.obsidian-version="1.12.7"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Workspace package.jsons + lockfile so npm ci sets up the @ignis/* symlinks.
|
||||
COPY package.json package-lock.json ./
|
||||
COPY packages/services/package.json ./packages/services/
|
||||
COPY packages/shim/package.json ./packages/shim/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
COPY packages/bridge-plugin/package.json ./packages/bridge-plugin/
|
||||
COPY packages/server-core/package.json ./packages/server-core/
|
||||
COPY apps/ignis-server/package.json ./apps/ignis-server/
|
||||
|
||||
RUN npm ci --omit=dev --ignore-scripts
|
||||
|
||||
# Runtime sources: the server, entrypoint script, repo-rooted images, and the server-core JS that gets required from the server at runtime.
|
||||
COPY apps/ignis-server/server/ ./apps/ignis-server/server/
|
||||
COPY apps/ignis-server/scripts/ ./apps/ignis-server/scripts/
|
||||
COPY images/ ./images/
|
||||
COPY packages/server-core/src/ ./packages/server-core/src/
|
||||
|
||||
# Bridge plugin: manifest + styles for auto-install into vaults; built main.js comes from the build stage.
|
||||
COPY packages/bridge-plugin/manifest.json ./packages/bridge-plugin/
|
||||
COPY packages/bridge-plugin/styles.css ./packages/bridge-plugin/
|
||||
|
||||
# Built artifacts from the build stage.
|
||||
COPY --from=build /app/packages/shim/dist/shim-loader.js ./packages/shim/dist/shim-loader.js
|
||||
COPY --from=build /app/packages/ui/dist/ignis-ui.js ./packages/ui/dist/ignis-ui.js
|
||||
COPY --from=build /app/packages/bridge-plugin/main.js ./packages/bridge-plugin/main.js
|
||||
COPY --from=build /app/apps/ignis-server/server/plugins/headless-sync/plugin/main.js ./apps/ignis-server/server/plugins/headless-sync/plugin/main.js
|
||||
|
||||
RUN chmod +x /app/apps/ignis-server/scripts/entrypoint.sh
|
||||
|
||||
ENV PORT=8080
|
||||
ENV VAULT_ROOT=/vaults
|
||||
ENV OBSIDIAN_VERSION=1.12.7
|
||||
ENV OBSIDIAN_ASSETS_PATH=/app/obsidian-app
|
||||
ENV PUID=1000
|
||||
ENV PGID=1000
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
VOLUME /vaults
|
||||
VOLUME /app/obsidian-app
|
||||
|
||||
ENTRYPOINT ["/app/apps/ignis-server/scripts/entrypoint.sh"]
|
||||
95
apps/ignis-server/README.md
Normal file
95
apps/ignis-server/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Ignis Server
|
||||
|
||||
The self-hosted Docker variant of Ignis. For the project overview, feature list, and what works / what doesn't, see the [root README](../../README.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- [Authentication](#authentication)
|
||||
- [Setup with Docker Compose](#setup-with-docker-compose)
|
||||
- [Volumes](#volumes)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Migrating an existing vault](#migrating-an-existing-vault)
|
||||
- [Upgrading Obsidian](#upgrading-obsidian)
|
||||
- [Backups](#backups)
|
||||
|
||||
## Authentication
|
||||
|
||||
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:
|
||||
|
||||
- A reverse proxy with Basic Auth (nginx, Caddy, Traefik)
|
||||
- An SSO proxy like Authelia, Authentik, or OAuth2 Proxy
|
||||
- A VPN (Tailscale, WireGuard)
|
||||
- Cloudflare Application Tunnel
|
||||
|
||||
Example configurations for Basic Auth and Authelia are in [`examples/`](examples).
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not run Ignis on a public network without auth. Anyone with the URL can read and write your vault files.
|
||||
|
||||
## Setup with Docker Compose
|
||||
|
||||
Example `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ignis:
|
||||
image: nobbe/ignis:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- OBSIDIAN_VERSION=1.12.7
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- ./vaults:/vaults
|
||||
- ./data:/app/data
|
||||
- obsidian-app:/app/obsidian-app
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
obsidian-app:
|
||||
```
|
||||
|
||||
Then `docker compose up -d`. On first start the container downloads Obsidian from the official source and installs the Obsidian Headless CLI. This takes a minute or two.
|
||||
|
||||
To build from source instead of pulling the image, clone the repo and run `docker compose up` against the [`docker-compose.yml`](docker-compose.yml) in this directory -- it is already wired to build from the monorepo root.
|
||||
|
||||
## Volumes
|
||||
|
||||
| Mount | Description |
|
||||
| ----- | ----------- |
|
||||
| `/vaults` | Vault storage. Each subdirectory is a vault. |
|
||||
| `/app/data` | State persistence for various Ignis-specific functionality: plugin management, headless sync config, etc. |
|
||||
| `/app/obsidian-app` | Cached Obsidian assets. Persisting this avoids re-downloading on container recreate. |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------- | ----------- | ------- |
|
||||
| `PORT` | Server listen port | `8080` |
|
||||
| `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 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 is no guarantee it will 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 does not provide a built-in backup mechanism.
|
||||
@@ -1,6 +1,8 @@
|
||||
services:
|
||||
ignis:
|
||||
build: .
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: apps/ignis-server/Dockerfile
|
||||
ports:
|
||||
- "8082:8080"
|
||||
environment:
|
||||
@@ -9,7 +9,8 @@
|
||||
services:
|
||||
ignis-demo:
|
||||
build:
|
||||
context: ../..
|
||||
context: ../../../..
|
||||
dockerfile: apps/ignis-server/Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
5
apps/ignis-server/package.json
Normal file
5
apps/ignis-server/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "@ignis/app",
|
||||
"version": "0.0.0-internal",
|
||||
"private": true
|
||||
}
|
||||
@@ -68,4 +68,4 @@ else
|
||||
fi
|
||||
|
||||
# Run as the determined user
|
||||
exec gosu "$RUN_USER" node /app/server/index.js
|
||||
exec gosu "$RUN_USER" node /app/apps/ignis-server/server/index.js
|
||||
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 984 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -41,8 +41,8 @@
|
||||
<div id="ignis-status-label">Loading Obsidian...</div>
|
||||
</div>
|
||||
<!-- Ignis shims: must run before any Obsidian code. -->
|
||||
<script type="text/javascript" src="__IGNIS_UI_SRC__"></script>
|
||||
<script type="text/javascript" src="__SHIM_LOADER_SRC__"></script>
|
||||
<script type="text/javascript" src="__IGNIS_UI_SRC__"></script>
|
||||
<!-- Obsidian scripts injected dynamically to avoid touching their files. -->
|
||||
<script>
|
||||
(function () {
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
} = require("./plugin-system/obsidian-plugin");
|
||||
|
||||
const BRIDGE_PLUGIN_ID = "ignis-bridge";
|
||||
const BRIDGE_PLUGIN_DIR = path.join(__dirname, "..", "plugin");
|
||||
const BRIDGE_PLUGIN_DIR = path.join(__dirname, "..", "..", "..", "packages", "bridge-plugin");
|
||||
|
||||
// .ignis metadata helpers
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..");
|
||||
|
||||
// VAULT_ROOT: a directory that contains vault folders.
|
||||
// Each subdirectory is a vault. New vaults are created as new subdirs.
|
||||
const vaultRoot =
|
||||
process.env.VAULT_ROOT || path.join(__dirname, "..", "vaults");
|
||||
process.env.VAULT_ROOT || path.join(REPO_ROOT, "vaults");
|
||||
|
||||
const dataRoot = process.env.DATA_ROOT || path.join(__dirname, "..", "data");
|
||||
const dataRoot = process.env.DATA_ROOT || path.join(REPO_ROOT, "data");
|
||||
|
||||
// Ensure required directories exist
|
||||
try {
|
||||
@@ -91,7 +93,7 @@ module.exports = {
|
||||
|
||||
obsidianAssetsPath:
|
||||
process.env.OBSIDIAN_ASSETS_PATH ||
|
||||
path.join(__dirname, "..", "investigation", "obsidian_1.12.7_unpacked"),
|
||||
path.join(REPO_ROOT, "investigation", "obsidian_1.12.7_unpacked"),
|
||||
|
||||
get obsidianVersion() {
|
||||
const assetsPath =
|
||||
@@ -5,7 +5,7 @@ const fsp = fs.promises;
|
||||
const path = require("path");
|
||||
|
||||
const config = require("../config");
|
||||
const watcher = require("../watcher");
|
||||
const { watcher } = require("@ignis/server-core");
|
||||
const bootstrapRoutes = require("../routes/bootstrap");
|
||||
|
||||
const {
|
||||
@@ -272,16 +272,36 @@ function trackVaultLifecycle(req, res, next) {
|
||||
|
||||
if (s) {
|
||||
if (req.path === "/create" && body.id) {
|
||||
s.vaults.add(body.id);
|
||||
// body.id is storage-prefixed at this point (outboundTranslator runs after us).
|
||||
// Translate to the user-visible name so it matches what pageLoadHandler queries with.
|
||||
const userName = tryParseUserVaultName(sessionId, body.id);
|
||||
|
||||
if (userName !== null) {
|
||||
s.vaults.add(userName);
|
||||
} else {
|
||||
console.warn(
|
||||
"[demo] trackVaultLifecycle: could not parse user name from create response id:",
|
||||
body.id,
|
||||
);
|
||||
}
|
||||
} else if (req.path === "/rename") {
|
||||
const oldName = req.body && req.body._origVault;
|
||||
const oldName = req._demoOriginalVault;
|
||||
|
||||
if (oldName) {
|
||||
s.vaults.delete(oldName);
|
||||
}
|
||||
|
||||
if (body.id) {
|
||||
s.vaults.add(body.id);
|
||||
const userName = tryParseUserVaultName(sessionId, body.id);
|
||||
|
||||
if (userName !== null) {
|
||||
s.vaults.add(userName);
|
||||
} else {
|
||||
console.warn(
|
||||
"[demo] trackVaultLifecycle: could not parse user name from rename response id:",
|
||||
body.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (req.method === "DELETE" && req.path === "/remove") {
|
||||
const removed = req._demoOriginalVault;
|
||||
@@ -4,14 +4,16 @@ const path = require("path");
|
||||
const compression = require("compression");
|
||||
const config = require("./config");
|
||||
const { getVersion } = require("./version");
|
||||
const { setupWebSocket } = require("./ws");
|
||||
const watcher = require("./watcher");
|
||||
const { setupWebSocket, watcher, writeCoalescer } = require("@ignis/server-core");
|
||||
const { updateBridgePluginInAllVaults } = require("./bridge-plugin");
|
||||
const { initPlugins, shutdownPlugins } = require("./plugin-system/manager");
|
||||
const pluginRoutes = require("./routes/plugins");
|
||||
const { flushAll } = require("./write-coalescer");
|
||||
writeCoalescer.configure({ writeCoalesceMs: config.writeCoalesceMs });
|
||||
const { flushAll } = writeCoalescer;
|
||||
const { setupDemo, wireDemoWebSocket } = require("./demo");
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..", "..");
|
||||
|
||||
const ANSI_RED = "\x1b[31m";
|
||||
const ANSI_YELLOW = "\x1b[33m";
|
||||
const ANSI_GREEN = "\x1b[32m";
|
||||
@@ -139,7 +141,7 @@ app.get(["/", "/index.html"], (req, res) => {
|
||||
});
|
||||
|
||||
app.get("/favicon.png", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "..", "images", "favicon.png"));
|
||||
res.sendFile(path.join(REPO_ROOT, "images", "favicon.png"));
|
||||
});
|
||||
|
||||
// Serve dist files with cache headers based on version param
|
||||
@@ -156,7 +158,8 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(express.static(path.join(__dirname, "..", "dist")));
|
||||
app.use(express.static(path.join(REPO_ROOT, "packages", "ui", "dist")));
|
||||
app.use(express.static(path.join(REPO_ROOT, "packages", "shim", "dist")));
|
||||
|
||||
app.use(express.static(config.obsidianAssetsPath));
|
||||
|
||||
@@ -172,7 +175,7 @@ const server = app.listen(config.port, async () => {
|
||||
.catch((e) => console.warn("[bootstrap] warm-up error:", e.message));
|
||||
});
|
||||
|
||||
const wss = setupWebSocket(server);
|
||||
const wss = setupWebSocket(server, { getVaultPath: config.getVaultPath });
|
||||
wireDemoWebSocket(server);
|
||||
|
||||
async function gracefulShutdown(signal) {
|
||||
@@ -1,10 +1,10 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
const { getObHome } = require("./ob-cli");
|
||||
|
||||
function getObAuthFile() {
|
||||
function getObAuthFile(dataDir) {
|
||||
return path.join(
|
||||
os.homedir(),
|
||||
getObHome(dataDir),
|
||||
".config",
|
||||
"obsidian-headless",
|
||||
"auth_token",
|
||||
@@ -23,14 +23,14 @@ function loadToken(dataDir) {
|
||||
const data = JSON.parse(fs.readFileSync(internalFile, "utf-8"));
|
||||
|
||||
if (data && data.token) {
|
||||
syncToObCli(data.token);
|
||||
syncToObCli(dataDir, data.token);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fall back to ob CLI's own auth file
|
||||
const obAuthFile = getObAuthFile();
|
||||
const obAuthFile = getObAuthFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(obAuthFile)) {
|
||||
@@ -49,7 +49,7 @@ function loadToken(dataDir) {
|
||||
|
||||
function saveToken(dataDir, tokenData) {
|
||||
saveInternal(dataDir, tokenData);
|
||||
syncToObCli(tokenData.token);
|
||||
syncToObCli(dataDir, tokenData.token);
|
||||
}
|
||||
|
||||
function clearToken(dataDir) {
|
||||
@@ -61,7 +61,7 @@ function clearToken(dataDir) {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const obAuthFile = getObAuthFile();
|
||||
const obAuthFile = getObAuthFile(dataDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(obAuthFile)) {
|
||||
@@ -94,8 +94,8 @@ function saveInternal(dataDir, tokenData) {
|
||||
fs.writeFileSync(internalFile, JSON.stringify(tokenData, null, 2), "utf-8");
|
||||
}
|
||||
|
||||
function syncToObCli(token) {
|
||||
const obAuthFile = getObAuthFile();
|
||||
function syncToObCli(dataDir, token) {
|
||||
const obAuthFile = getObAuthFile(dataDir);
|
||||
|
||||
try {
|
||||
const dir = path.dirname(obAuthFile);
|
||||
@@ -124,4 +124,10 @@ function getTokenInfo(dataDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { loadToken, saveToken, clearToken, isAuthenticated, getTokenInfo };
|
||||
module.exports = {
|
||||
loadToken,
|
||||
saveToken,
|
||||
clearToken,
|
||||
isAuthenticated,
|
||||
getTokenInfo,
|
||||
};
|
||||
@@ -29,6 +29,10 @@ module.exports = {
|
||||
ctx.log("ob CLI not found. Install obsidian-headless to enable sync.");
|
||||
}
|
||||
|
||||
// Redirect ob's HOME under the plugin's data dir so its config (per-vault sync setups, etc.)
|
||||
// survives container recreates. Must happen before auth.loadToken since loadToken pushes the token into ob's config location via syncToObCli.
|
||||
obCli.configure({ dataDir: ctx.dataDir });
|
||||
|
||||
const token = auth.loadToken(ctx.dataDir);
|
||||
|
||||
if (token) {
|
||||
@@ -1,8 +1,28 @@
|
||||
const { spawn, execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
// When set via configure(), HOME for the spawned ob points under the plugin's data dir so
|
||||
// ob's config dir (~/.config/obsidian-headless/) survives container recreates.
|
||||
let configuredDataDir = null;
|
||||
|
||||
function getObHome(dataDir) {
|
||||
return path.join(dataDir, "ob-home");
|
||||
}
|
||||
|
||||
function configure(opts) {
|
||||
configuredDataDir = opts && opts.dataDir ? opts.dataDir : null;
|
||||
|
||||
if (configuredDataDir) {
|
||||
try {
|
||||
fs.mkdirSync(getObHome(configuredDataDir), { recursive: true });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function checkInstalled() {
|
||||
try {
|
||||
const output = execSync("ob --version", {
|
||||
@@ -19,8 +39,12 @@ function checkInstalled() {
|
||||
}
|
||||
|
||||
function spawnOb(args, opts = {}) {
|
||||
const home = configuredDataDir
|
||||
? getObHome(configuredDataDir)
|
||||
: os.homedir();
|
||||
|
||||
return spawn("ob", args, {
|
||||
env: { ...process.env, HOME: os.homedir() },
|
||||
env: { ...process.env, HOME: home },
|
||||
shell: isWindows,
|
||||
windowsHide: true,
|
||||
...opts,
|
||||
@@ -58,4 +82,10 @@ function runCommand(args, opts = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { checkInstalled, spawnOb, runCommand };
|
||||
module.exports = {
|
||||
checkInstalled,
|
||||
spawnOb,
|
||||
runCommand,
|
||||
configure,
|
||||
getObHome,
|
||||
};
|
||||
@@ -3,59 +3,16 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const archiver = require("archiver");
|
||||
const config = require("../config");
|
||||
const { writeCoalesced, getPending } = require("../write-coalescer");
|
||||
const {
|
||||
writeCoalescer,
|
||||
encodeContentDispositionFilename,
|
||||
resolveVaultPath,
|
||||
} = require("@ignis/server-core");
|
||||
const { writeCoalesced, getPending } = writeCoalescer;
|
||||
const bootstrapRoutes = require("./bootstrap");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Encode a filename for use in Content-Disposition header.
|
||||
* Handles non-ASCII characters and special characters to prevent header injection.
|
||||
* Uses RFC 5987 encoding for filename* parameter when needed.
|
||||
*
|
||||
* @param {string} filename - The filename to encode
|
||||
* @returns {string} - Properly formatted Content-Disposition value
|
||||
*/
|
||||
function encodeContentDispositionFilename(filename) {
|
||||
// Check if filename contains non-ASCII characters
|
||||
const hasNonASCII = /[^\x00-\x7F]/.test(filename);
|
||||
|
||||
// Escape quotes and backslashes in ASCII filename by prefixing with backslash
|
||||
const escapedFilename = filename.replace(/["\\ ]/g, function (match) {
|
||||
if (match === '"') return '\\"';
|
||||
if (match === "\\") return "\\\\";
|
||||
return match;
|
||||
});
|
||||
|
||||
// Remove any control characters that could cause header injection
|
||||
const sanitizedFilename = escapedFilename.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
|
||||
if (!hasNonASCII) {
|
||||
// Simple ASCII filename - use standard format
|
||||
return `attachment; filename="${sanitizedFilename}"`;
|
||||
}
|
||||
|
||||
// Non-ASCII filename - use RFC 5987 encoding
|
||||
// Encode using percent-encoding for UTF-8
|
||||
const encodedFilename = encodeURIComponent(filename)
|
||||
.replace(/['()]/g, function (c) {
|
||||
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
})
|
||||
.replace(/\*/g, "%2A");
|
||||
|
||||
// Provide both filename (ASCII fallback) and filename* (UTF-8 encoded)
|
||||
// For fallback, replace non-ASCII with underscores
|
||||
const asciiFallback = filename
|
||||
.replace(/[^\x00-\x7F]/g, "_")
|
||||
.replace(/["\\ ]/g, function (match) {
|
||||
if (match === '"') return '\\"';
|
||||
if (match === "\\") return "\\\\";
|
||||
return match;
|
||||
});
|
||||
|
||||
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodedFilename}`;
|
||||
}
|
||||
|
||||
// Resolve the vault root for a request. Reads vault ID from query or body.
|
||||
function getVaultRoot(req, res) {
|
||||
const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId;
|
||||
@@ -76,20 +33,6 @@ function invalidateBootstrap(req) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a client-provided path to an absolute path within a vault.
|
||||
// Strips leading slashes so paths from the client are always treated as relative to the vault root.
|
||||
function resolveVaultPath(vaultRoot, relativePath) {
|
||||
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
||||
const resolved = path.resolve(vaultRoot, cleaned);
|
||||
|
||||
const resolvedRoot = path.resolve(vaultRoot);
|
||||
|
||||
if (resolved !== resolvedRoot && !resolved.startsWith(resolvedRoot + path.sep)) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function guardPath(req, res, source = "query") {
|
||||
const vaultRoot = getVaultRoot(req, res);
|
||||
|
||||
@@ -653,5 +596,3 @@ router.get("/download-zip", async (req, res) => {
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports.resolveVaultPath = resolveVaultPath;
|
||||
module.exports.encodeContentDispositionFilename = encodeContentDispositionFilename;
|
||||
@@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
|
||||
const {
|
||||
resolveVaultPath,
|
||||
encodeContentDispositionFilename,
|
||||
} = require("./fs.js");
|
||||
} = require("@ignis/server-core");
|
||||
|
||||
// -- encodeContentDispositionFilename --------------------------------
|
||||
|
||||
54
build.js
54
build.js
@@ -1,59 +1,23 @@
|
||||
const esbuild = require("esbuild");
|
||||
const sveltePlugin = require("esbuild-svelte");
|
||||
const path = require("path");
|
||||
|
||||
const { version: ignisVersion } = require("./package.json");
|
||||
|
||||
Promise.all([
|
||||
// Build shim-loader.js
|
||||
esbuild.build({
|
||||
entryPoints: [path.join(__dirname, "src", "shims", "loader.js")],
|
||||
bundle: true,
|
||||
outfile: path.join(__dirname, "dist", "shim-loader.js"),
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: ["chrome90"],
|
||||
alias: {
|
||||
path: "path-browserify",
|
||||
},
|
||||
define: {
|
||||
__IGNIS_VERSION__: JSON.stringify(ignisVersion),
|
||||
},
|
||||
logLevel: "info",
|
||||
}),
|
||||
// Build shim-loader.js (delegated to packages/shim)
|
||||
require("./packages/shim/build.js"),
|
||||
|
||||
// Build ignis-ui.js
|
||||
esbuild.build({
|
||||
entryPoints: [path.join(__dirname, "src", "ui", "index.js")],
|
||||
bundle: true,
|
||||
outfile: path.join(__dirname, "dist", "ignis-ui.js"),
|
||||
format: "iife",
|
||||
globalName: "IgnisUI",
|
||||
platform: "browser",
|
||||
target: ["chrome90"],
|
||||
mainFields: ["svelte", "browser", "module", "main"],
|
||||
conditions: ["svelte", "browser"],
|
||||
plugins: [sveltePlugin({ compilerOptions: { css: "injected" } })],
|
||||
logLevel: "info",
|
||||
}),
|
||||
// Build ignis-ui.js (delegated to packages/ui)
|
||||
require("./packages/ui/build.js"),
|
||||
|
||||
// Build ignis-bridge plugin
|
||||
esbuild.build({
|
||||
entryPoints: [path.join(__dirname, "plugin", "src", "main.js")],
|
||||
bundle: true,
|
||||
outfile: path.join(__dirname, "plugin", "main.js"),
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
target: ["chrome90"],
|
||||
external: ["obsidian", "fs"],
|
||||
logLevel: "info",
|
||||
}),
|
||||
// Build ignis-bridge plugin (delegated to packages/bridge-plugin)
|
||||
require("./packages/bridge-plugin/build.js"),
|
||||
|
||||
// Build headless-sync bundled plugin
|
||||
esbuild.build({
|
||||
entryPoints: [
|
||||
path.join(
|
||||
__dirname,
|
||||
"apps",
|
||||
"ignis-server",
|
||||
"server",
|
||||
"plugins",
|
||||
"headless-sync",
|
||||
@@ -65,6 +29,8 @@ Promise.all([
|
||||
bundle: true,
|
||||
outfile: path.join(
|
||||
__dirname,
|
||||
"apps",
|
||||
"ignis-server",
|
||||
"server",
|
||||
"plugins",
|
||||
"headless-sync",
|
||||
|
||||
@@ -43,7 +43,7 @@ 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 on disk, or transformed in transit.
|
||||
The server serves its own `index.html` (in `apps/ignis-server/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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -77,11 +77,11 @@ Two caches on the client side. The **MetadataCache** holds `{ type, size, mtime,
|
||||
|
||||
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`).
|
||||
|
||||
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.
|
||||
Writes go through a server-side write coalescer (`packages/server-core/src/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.
|
||||
|
||||
### Transforms
|
||||
|
||||
The shim has a transforms registry (`src/shims/fs/transforms.js`) for hooks applied at the public shim surface, before caches or transport see the path. Three hook types:
|
||||
The shim has a transforms registry (`packages/shim/src/fs/transforms.js`) for hooks applied at the public shim surface, before caches or transport see the path. Three hook types:
|
||||
|
||||
- **Path resolvers** map a logical path to a physical path. Used by the workspaces shim to redirect reads and writes of `.obsidian/workspace.json` to `.obsidian/workspace.<name>.json` based on the `?workspace=` URL parameter, so each browser tab can hold a separate layout.
|
||||
- **Read transforms** post-process bytes returned by a read (cache hit or transport miss). Used to mask the Obsidian Sync setting in `core-plugins.json` when headless-sync is active for the vault, and to override the `active` field on reads of `workspaces.json` so each tab sees its own workspace as selected.
|
||||
@@ -143,7 +143,7 @@ Standard community and core Obsidian plugins. Obsidian evals plugin code with it
|
||||
|
||||
### Bridge Plugin (ignis-bridge)
|
||||
|
||||
An Obsidian plugin auto-installed into every vault by the server. Source lives in `plugin/`, built to `plugin/main.js`.
|
||||
An Obsidian plugin auto-installed into every vault by the server. Source lives in `packages/bridge-plugin/`, built to `packages/bridge-plugin/main.js`.
|
||||
|
||||
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).
|
||||
@@ -158,11 +158,11 @@ Not user-installable through Obsidian's plugin browser. Managed entirely by the
|
||||
|
||||
A basic plugin system for extending the server. Still early, the core lifecycle works but the API surface is minimal and likely to change.
|
||||
|
||||
An Ignis plugin is a Node.js package under `server/plugins/<name>/` that exports an id, name, and a `register` function. On load it receives a context object with access to config, the WebSocket server, a file watcher, an Express router, a logger, and a persistent data directory. Plugins are enabled and disabled per vault, with state persisted in `data/plugin-config.json`.
|
||||
An Ignis plugin is a Node.js package under `apps/ignis-server/server/plugins/<name>/` that exports an id, name, and a `register` function. On load it receives a context object with access to config, the WebSocket server, a file watcher, an Express router, a logger, and a persistent data directory. Plugins are enabled and disabled per vault, with state persisted in `data/plugin-config.json`.
|
||||
|
||||
When enabled, a plugin's Express router is mounted at `/api/ext/<pluginId>/`. 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.
|
||||
The one Ignis plugin currently in the repo is **headless-sync** (`apps/ignis-server/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
|
||||
|
||||
@@ -179,4 +179,4 @@ Other demo behaviors:
|
||||
- Server-side plugins (e.g. headless-sync) hidden from the client; enable/disable returns 403.
|
||||
- The bridge plugin disables any `<input type="email">` or `<input type="password">` it sees anywhere in the document, with a placeholder telling users not to enter credentials.
|
||||
|
||||
All server-side demo code lives in `server/demo/`. The client-side hooks live in `src/shims/demo.js`. The deployment example is in `examples/demo/` (tmpfs-mounted vaults, restricted proxy, all the env vars).
|
||||
All server-side demo code lives in `apps/ignis-server/server/demo/`. The client-side hooks live in `packages/shim/src/demo.js`. The deployment example is in `apps/ignis-server/examples/demo/` (tmpfs-mounted vaults, restricted proxy, all the env vars).
|
||||
108
package-lock.json
generated
108
package-lock.json
generated
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"name": "ignis",
|
||||
"name": "ignis-monorepo",
|
||||
"version": "0.8.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ignis",
|
||||
"name": "ignis-monorepo",
|
||||
"version": "0.8.1",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"chokidar": "^3.6.0",
|
||||
@@ -26,11 +30,19 @@
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
},
|
||||
"apps/ignis": {
|
||||
"name": "@ignis/app",
|
||||
"version": "0.8.1",
|
||||
"extraneous": true
|
||||
},
|
||||
"apps/ignis-server": {
|
||||
"name": "@ignis/app",
|
||||
"version": "0.0.0-internal"
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -482,6 +494,30 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@ignis/app": {
|
||||
"resolved": "apps/ignis-server",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ignis/bridge-plugin": {
|
||||
"resolved": "packages/bridge-plugin",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ignis/server-core": {
|
||||
"resolved": "packages/server-core",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ignis/services": {
|
||||
"resolved": "packages/services",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ignis/shim": {
|
||||
"resolved": "packages/shim",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@ignis/ui": {
|
||||
"resolved": "packages/ui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -503,7 +539,6 @@
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -514,7 +549,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -524,14 +558,12 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -542,7 +574,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
@@ -972,7 +1003,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
@@ -1119,7 +1149,6 @@
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -1205,7 +1234,6 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1237,7 +1265,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -1567,7 +1594,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
||||
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
@@ -1752,7 +1778,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
@@ -1944,7 +1969,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
@@ -2369,7 +2393,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
@@ -2467,7 +2490,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
@@ -2493,7 +2515,6 @@
|
||||
"version": "0.577.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.577.0.tgz",
|
||||
"integrity": "sha512-0i88o57KsaHWnc80J57fY99CWzlZsSdtH5kKjLUJa7z8dum/9/AbINNLzJ7NiRFUdOgMnfAmJt8jFbW2zeC5qQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3 || ^4 || ^5.0.0-next.42"
|
||||
@@ -2503,7 +2524,6 @@
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -2522,7 +2542,6 @@
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
@@ -2719,7 +2738,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
@@ -2774,7 +2792,6 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
@@ -3184,7 +3201,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -3346,7 +3362,6 @@
|
||||
"version": "4.2.20",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz",
|
||||
"integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
@@ -4412,6 +4427,51 @@
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"packages/bridge-plugin": {
|
||||
"name": "@ignis/bridge-plugin",
|
||||
"version": "0.0.0-internal",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
}
|
||||
},
|
||||
"packages/server-core": {
|
||||
"name": "@ignis/server-core",
|
||||
"version": "0.0.0-internal",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
},
|
||||
"packages/services": {
|
||||
"name": "@ignis/services",
|
||||
"version": "0.0.0-internal"
|
||||
},
|
||||
"packages/shim": {
|
||||
"name": "@ignis/shim",
|
||||
"version": "0.0.0-internal",
|
||||
"dependencies": {
|
||||
"@ignis/services": "*",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"pako": "^2.1.0",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
}
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@ignis/ui",
|
||||
"version": "0.0.0-internal",
|
||||
"dependencies": {
|
||||
"@ignis/services": "*",
|
||||
"lucide-svelte": "^0.577.0",
|
||||
"svelte": "^4.2.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0",
|
||||
"esbuild-svelte": "^0.9.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": "ignis",
|
||||
"name": "ignis-monorepo",
|
||||
"version": "0.8.1",
|
||||
"private": true,
|
||||
"description": "An Electron shim and server bridge for running Obsidian in a browser.",
|
||||
"description": "Monorepo for Ignis: a browser-based Obsidian client. Self-hosted server in apps/ignis-server; shim, UI, and shared libraries in packages/.",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"dev:server": "node server/index.js",
|
||||
"dev:server": "node apps/ignis-server/server/index.js",
|
||||
"dev": "npm run build && npm run dev:server",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
|
||||
13
packages/bridge-plugin/build.js
Normal file
13
packages/bridge-plugin/build.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const esbuild = require("esbuild");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = esbuild.build({
|
||||
entryPoints: [path.join(__dirname, "src", "main.js")],
|
||||
bundle: true,
|
||||
outfile: path.join(__dirname, "main.js"),
|
||||
format: "cjs",
|
||||
platform: "browser",
|
||||
target: ["chrome90"],
|
||||
external: ["obsidian", "fs"],
|
||||
logLevel: "info",
|
||||
});
|
||||
11
packages/bridge-plugin/package.json
Normal file
11
packages/bridge-plugin/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@ignis/bridge-plugin",
|
||||
"version": "0.0.0-internal",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
}
|
||||
}
|
||||
10
packages/server-core/package.json
Normal file
10
packages/server-core/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@ignis/server-core",
|
||||
"version": "0.0.0-internal",
|
||||
"private": true,
|
||||
"main": "src/index.js",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"ws": "^8.16.0"
|
||||
}
|
||||
}
|
||||
15
packages/server-core/src/index.js
Normal file
15
packages/server-core/src/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const writeCoalescer = require("./write-coalescer");
|
||||
const watcher = require("./watcher");
|
||||
const { setupWebSocket } = require("./ws");
|
||||
const {
|
||||
encodeContentDispositionFilename,
|
||||
resolveVaultPath,
|
||||
} = require("./path-utils");
|
||||
|
||||
module.exports = {
|
||||
writeCoalescer,
|
||||
watcher,
|
||||
setupWebSocket,
|
||||
encodeContentDispositionFilename,
|
||||
resolveVaultPath,
|
||||
};
|
||||
64
packages/server-core/src/path-utils.js
Normal file
64
packages/server-core/src/path-utils.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Encode a filename for use in Content-Disposition header.
|
||||
* Handles non-ASCII characters and special characters to prevent header injection.
|
||||
* Uses RFC 5987 encoding for filename* parameter when needed.
|
||||
*/
|
||||
function encodeContentDispositionFilename(filename) {
|
||||
const hasNonASCII = /[^\x00-\x7F]/.test(filename);
|
||||
|
||||
// Escape quotes and backslashes in ASCII filename
|
||||
const escapedFilename = filename.replace(/["\\ ]/g, function (match) {
|
||||
if (match === '"') return '\\"';
|
||||
if (match === "\\") return "\\\\";
|
||||
return match;
|
||||
});
|
||||
|
||||
// Remove any control characters that could cause header injection
|
||||
const sanitizedFilename = escapedFilename.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
|
||||
if (!hasNonASCII) {
|
||||
// Simple ASCII filename - use standard format
|
||||
return `attachment; filename="${sanitizedFilename}"`;
|
||||
}
|
||||
|
||||
// Non-ASCII filename - use RFC 5987 encoding
|
||||
// Encode using percent-encoding for UTF-8
|
||||
const encodedFilename = encodeURIComponent(filename)
|
||||
.replace(/['()]/g, function (c) {
|
||||
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
|
||||
})
|
||||
.replace(/\*/g, "%2A");
|
||||
|
||||
// Provide both filename (ASCII fallback) and filename* (UTF-8 encoded)
|
||||
// For fallback, replace non-ASCII with underscores
|
||||
const asciiFallback = filename
|
||||
.replace(/[^\x00-\x7F]/g, "_")
|
||||
.replace(/["\\ ]/g, function (match) {
|
||||
if (match === '"') return '\\"';
|
||||
if (match === "\\") return "\\\\";
|
||||
return match;
|
||||
});
|
||||
|
||||
return `attachment; filename="${asciiFallback}"; filename*=UTF-8''${encodedFilename}`;
|
||||
}
|
||||
|
||||
// Resolve a client-provided path to an absolute path within a vault.
|
||||
// Strips leading slashes so paths from the client are always treated as relative to the vault root.
|
||||
function resolveVaultPath(vaultRoot, relativePath) {
|
||||
const cleaned = (relativePath || "").replace(/^\/+/, "");
|
||||
const resolved = path.resolve(vaultRoot, cleaned);
|
||||
|
||||
const resolvedRoot = path.resolve(vaultRoot);
|
||||
|
||||
if (
|
||||
resolved !== resolvedRoot &&
|
||||
!resolved.startsWith(resolvedRoot + path.sep)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
module.exports = { encodeContentDispositionFilename, resolveVaultPath };
|
||||
@@ -5,10 +5,18 @@
|
||||
// Buffered writes respond to the HTTP client right away with synthetic mtime/size. Otherwise the browser's per-host connection cap blocks unrelated reads while writes sit in the buffer.
|
||||
|
||||
const fs = require("fs");
|
||||
const config = require("./config");
|
||||
|
||||
const FLUSH_TIMEOUT_MS = 10000;
|
||||
|
||||
// Coalesce window in ms. 0 disables coalescing. Set via configure({ writeCoalesceMs }).
|
||||
let writeCoalesceMs = 0;
|
||||
|
||||
function configure(opts) {
|
||||
if (typeof opts?.writeCoalesceMs === "number") {
|
||||
writeCoalesceMs = opts.writeCoalesceMs;
|
||||
}
|
||||
}
|
||||
|
||||
// absPath -> timestamp of last completed (or scheduled) write
|
||||
const lastWriteTime = new Map();
|
||||
|
||||
@@ -51,7 +59,7 @@ function scheduleFlush(absPath) {
|
||||
}
|
||||
|
||||
clearTimeout(entry.timer);
|
||||
entry.timer = setTimeout(() => flushEntry(absPath), config.writeCoalesceMs);
|
||||
entry.timer = setTimeout(() => flushEntry(absPath), writeCoalesceMs);
|
||||
}
|
||||
|
||||
function estimateSize(data, encoding) {
|
||||
@@ -67,7 +75,7 @@ function estimateSize(data, encoding) {
|
||||
* Fresh writes resolve with real mtime/size once data is on disk. Buffered writes resolve immediately with synthetic values; the disk flush happens later when the debounce timer fires.
|
||||
*/
|
||||
async function writeCoalesced(absPath, data, encoding) {
|
||||
const windowMs = config.writeCoalesceMs;
|
||||
const windowMs = writeCoalesceMs;
|
||||
const last = lastWriteTime.get(absPath);
|
||||
|
||||
// Fast path: coalescing disabled or far enough from the last write.
|
||||
@@ -159,4 +167,4 @@ function _reset() {
|
||||
lastWriteTime.clear();
|
||||
}
|
||||
|
||||
module.exports = { writeCoalesced, getPending, flushAll, _reset };
|
||||
module.exports = { writeCoalesced, getPending, flushAll, configure, _reset };
|
||||
@@ -6,15 +6,13 @@ import os from "os";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const coalescer = require("./write-coalescer.js");
|
||||
const config = require("./config.js");
|
||||
|
||||
const SHORT_WINDOW_MS = 50;
|
||||
const originalWindow = config.writeCoalesceMs;
|
||||
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(async () => {
|
||||
config.writeCoalesceMs = SHORT_WINDOW_MS;
|
||||
coalescer.configure({ writeCoalesceMs: SHORT_WINDOW_MS });
|
||||
coalescer._reset();
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "coalesce-test-"));
|
||||
});
|
||||
@@ -22,7 +20,7 @@ beforeEach(async () => {
|
||||
afterEach(async () => {
|
||||
coalescer._reset();
|
||||
vi.restoreAllMocks();
|
||||
config.writeCoalesceMs = originalWindow;
|
||||
coalescer.configure({ writeCoalesceMs: 0 });
|
||||
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
const { WebSocketServer } = require("ws");
|
||||
const url = require("url");
|
||||
const config = require("./config");
|
||||
const watcher = require("./watcher");
|
||||
|
||||
function setupWebSocket(server) {
|
||||
function setupWebSocket(server, opts = {}) {
|
||||
const { getVaultPath } = opts;
|
||||
|
||||
if (typeof getVaultPath !== "function") {
|
||||
throw new Error("setupWebSocket: opts.getVaultPath is required");
|
||||
}
|
||||
|
||||
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||
|
||||
// Plugin-registered message handlers: type -> handler(msg, ws)
|
||||
@@ -13,12 +18,12 @@ function setupWebSocket(server) {
|
||||
const params = new url.URL(req.url, "http://localhost").searchParams;
|
||||
const vaultId = params.get("vault");
|
||||
|
||||
if (!vaultId || !config.getVaultPath(vaultId)) {
|
||||
if (!vaultId || !getVaultPath(vaultId)) {
|
||||
ws.close(4001, "Invalid or missing vault ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const vaultPath = config.getVaultPath(vaultId);
|
||||
const vaultPath = getVaultPath(vaultId);
|
||||
console.log(`[ws] Client connected to vault: ${vaultId}`);
|
||||
|
||||
// Start watching this vault (no-op if already watching)
|
||||
6
packages/services/package.json
Normal file
6
packages/services/package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@ignis/services",
|
||||
"version": "0.0.0-internal",
|
||||
"private": true,
|
||||
"main": "src/index.js"
|
||||
}
|
||||
1
packages/services/src/index.js
Normal file
1
packages/services/src/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { vaultService } from "./vault-service.js";
|
||||
20
packages/shim/build.js
Normal file
20
packages/shim/build.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const esbuild = require("esbuild");
|
||||
const path = require("path");
|
||||
|
||||
const { version: ignisVersion } = require("../../package.json");
|
||||
|
||||
module.exports = esbuild.build({
|
||||
entryPoints: [path.join(__dirname, "src", "loader.js")],
|
||||
bundle: true,
|
||||
outfile: path.join(__dirname, "dist", "shim-loader.js"),
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: ["chrome90"],
|
||||
alias: {
|
||||
path: "path-browserify",
|
||||
},
|
||||
define: {
|
||||
__IGNIS_VERSION__: JSON.stringify(ignisVersion),
|
||||
},
|
||||
logLevel: "info",
|
||||
});
|
||||
18
packages/shim/package.json
Normal file
18
packages/shim/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@ignis/shim",
|
||||
"version": "0.0.0-internal",
|
||||
"private": true,
|
||||
"main": "src/loader.js",
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ignis/services": "*",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"pako": "^2.1.0",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.20.0"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user