dev.sh still has to opt into the relay profile now that it's no longer in the default set; otherwise `./dev.sh` would print "relay: http://localhost:8081" but skip starting the container. README gets a short compose section in Quick Start explaining the app-only default and how to add --profile relay for a local relay. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
544 lines
20 KiB
Markdown
544 lines
20 KiB
Markdown
# hyprcrd
|
||
|
||
A self-hosted card/note manager with markdown, tags, image galleries, and Obsidian-compatible export.
|
||
|
||
## Requirements
|
||
|
||
- PHP 8.3+
|
||
- Apache with `mod_rewrite`
|
||
- PHP extensions: `pdo_sqlite`, `zip`
|
||
|
||
No external dependencies. All assets are bundled locally.
|
||
|
||
## Quick Start (Docker)
|
||
|
||
```bash
|
||
git clone https://git.uesome.de/tanztee/hyprcrd.git
|
||
cd hyprcrd
|
||
docker build -t hyprcrd .
|
||
docker run -d -p 8080:80 -v hyprcrd-data:/var/www/html/data hyprcrd
|
||
```
|
||
|
||
Open `http://localhost:8080` and set your password on first visit.
|
||
|
||
The `-v hyprcrd-data:/var/www/html/data` flag persists the database across container restarts.
|
||
|
||
### Via docker compose
|
||
|
||
If you prefer compose, the bundled `docker-compose.yml` starts the app alone by default:
|
||
|
||
```bash
|
||
docker compose up -d # just the app on :8088
|
||
```
|
||
|
||
To also bring up a local relay (for multi-tenant messaging / collab / chat), opt in via the `relay` profile:
|
||
|
||
```bash
|
||
docker compose --profile relay up -d # app on :8088 + relay on :8081
|
||
```
|
||
|
||
Most installs talk to a remote relay, so the app-only default keeps the local setup minimal.
|
||
|
||
## Manual Installation
|
||
|
||
### 1. Clone
|
||
|
||
```bash
|
||
git clone https://git.uesome.de/tanztee/hyprcrd.git
|
||
cd hyprcrd
|
||
```
|
||
|
||
### 2. Set document root
|
||
|
||
Point your web server's document root to the `public/` directory.
|
||
|
||
**Apache vhost example:**
|
||
|
||
```apache
|
||
<VirtualHost *:80>
|
||
ServerName hyprcrd.local
|
||
DocumentRoot /path/to/hyprcrd/public
|
||
|
||
<Directory /path/to/hyprcrd/public>
|
||
AllowOverride All
|
||
Require all granted
|
||
</Directory>
|
||
</VirtualHost>
|
||
```
|
||
|
||
Make sure `mod_rewrite` is enabled:
|
||
|
||
```bash
|
||
a2enmod rewrite
|
||
systemctl restart apache2
|
||
```
|
||
|
||
### 3. Set permissions
|
||
|
||
```bash
|
||
mkdir -p data public/uploads
|
||
chmod 755 data
|
||
chmod 777 public/uploads
|
||
```
|
||
|
||
The web server user (e.g. `www-data`) needs write access to both directories.
|
||
|
||
### 4. PHP settings (optional)
|
||
|
||
For larger image uploads, adjust in `php.ini`:
|
||
|
||
```ini
|
||
upload_max_filesize = 20M
|
||
post_max_size = 22M
|
||
```
|
||
|
||
### 5. Open and set password
|
||
|
||
Navigate to your URL. On first visit, you'll be prompted to set a password (min. 4 characters). That's it.
|
||
|
||
## Directory Structure
|
||
|
||
```
|
||
hyprcrd/
|
||
├── config.php # App constants (auto-configured)
|
||
├── Dockerfile
|
||
├── docker-entrypoint.sh
|
||
├── dist.sh # Build distribution ZIP
|
||
├── src/
|
||
│ ├── bootstrap.php # Initialization + plugin discovery
|
||
│ ├── Database.php # SQLite schema & migrations
|
||
│ ├── PluginManager.php # Plugin scanner & asset/slot renderer
|
||
│ ├── CalDavClient.php # CalDAV HTTP client + iCal parser
|
||
│ └── CalDavSync.php # Bidirectional CalDAV sync engine
|
||
├── public/ # Document root
|
||
│ ├── index.php # Entry point & API
|
||
│ ├── .htaccess # URL rewriting
|
||
│ ├── css/
|
||
│ │ ├── style.css # App styles
|
||
│ │ ├── tailwind.out.css # Generated Tailwind utilities
|
||
│ │ ├── fonts.css # Local @font-face rules
|
||
│ │ └── vendor/ # Phosphor Icons CSS + fonts
|
||
│ ├── fonts/ # JetBrains Mono & Silkscreen
|
||
│ ├── js/
|
||
│ │ ├── hyprcrd.js # Plugin API (window.hyprcrd, sync-loaded)
|
||
│ │ ├── app.js # Orchestrator
|
||
│ │ ├── modules/ # Core feature modules
|
||
│ │ └── vendor/ # marked.js, DOMPurify
|
||
│ ├── plugins/ # Optional, drop-in plugin folders
|
||
│ │ ├── canvas/ # ASCII art canvas editor
|
||
│ │ ├── chat/ # Encrypted relay chat
|
||
│ │ ├── collab-buckets/ # Collaborative bucket sync
|
||
│ │ ├── email/ # Email overlay
|
||
│ │ ├── gitea/ # Gitea issue integration
|
||
│ │ ├── logseq-import/ # Logseq markdown importer
|
||
│ │ ├── slideshow/ # Presentation mode
|
||
│ │ └── tests/ # UI + state test runner
|
||
│ └── uploads/ # User media (writable)
|
||
├── electron/ # Electron desktop app wrapper
|
||
│ ├── main.js # Spawns PHP, creates BrowserWindow
|
||
│ ├── router.php # PHP built-in server router
|
||
│ ├── fetch-php.sh # Downloads static PHP binaries
|
||
│ └── php-bin/ # Platform-specific PHP binaries
|
||
├── relay/ # Relay service (multi-tenant messaging + collab)
|
||
│ ├── index.php # Entry point + API dispatch
|
||
│ ├── inc/ # Backend modules (bootstrap, crypto, auth, routes)
|
||
│ ├── ui/ # Frontend (layout.php, style.css, app.js)
|
||
│ ├── .htaccess # URL rewriting + data protection
|
||
│ ├── Dockerfile
|
||
│ └── dist.sh # Build relay distribution ZIP
|
||
└── data/
|
||
└── hyprcrd.db # SQLite database (auto-created)
|
||
```
|
||
|
||
## Features
|
||
|
||
- **Cards** with markdown body, color, tags, and position
|
||
- **Buckets** for organizing cards into groups
|
||
- **Tags** written as `#tagname` or hierarchical `#parent/child` in card body, extracted automatically
|
||
- **Markdown** with `[[Card Title]]` wiki-links, ```` ```gallery ```` image galleries, ```` ```tree ```` list views, ```` ```ascii ```` art canvas (via canvas plugin)
|
||
- **Search** combining text and `#tag` filters (e.g. `#project meeting notes`)
|
||
- **Assets gallery** (images, audio, PDFs) with orphan detection and fullscreen viewer
|
||
- **Split view** showing tag-related and content-related cards
|
||
- **Import/Export** as JSON+media ZIP or Obsidian/Logseq-compatible markdown vault
|
||
- **Drag & drop** card reordering and grouping
|
||
- **Calendar** with month view, journal entries, appointments, and day popup
|
||
- **CalDAV sync** with ownCloud/Nextcloud (bidirectional, `#termin` tagged cards)
|
||
- **Inline calculator** (`?3+5` → `8`, `34+56?` → `34+56=90`)
|
||
- **Snippets** (`?dat`, `?time`, `?now`, `?todo`, `?table`, `?code`, ...)
|
||
- **Due dates** with overdue notifications and pulsing cards
|
||
- **Relay** for mobile note capture and card sharing between instances
|
||
- **Notification banner** for upcoming events, birthdays, overdue tasks
|
||
- **Board split view** with side-by-side preview
|
||
- **Superview** compact multi-column list with hover cards
|
||
- **Super-buckets** — nest buckets one level deep with a dropdown for children
|
||
- **Card groups** — drag-to-group, join multiple cards, dissolve
|
||
- **Context menu** — color, tags, copy text, mark done, join cards, make ticket (gitea)
|
||
- **Find & replace** — in-card (Ctrl+H) and across all cards (Ctrl+Shift+H via search)
|
||
- **Arrow key navigation** on the board with Enter to open
|
||
- **PDF upload** in the editor toolbar
|
||
- **Keyboard shortcuts**: `Ctrl+K` search, `F1` help, `?` shortcut reference, arrow keys, 1–9 bucket switch
|
||
- **Electron desktop app** — standalone AppImage/DMG/NSIS with bundled PHP, no server needed
|
||
|
||
## Plugins
|
||
|
||
hyprcrd has a drop-in plugin system. A plugin is just a folder under `public/plugins/` with a `plugin.json` manifest. The core scans the directory at startup, loads each plugin's CSS/JS/HTML, and the plugin self-registers against `window.hyprcrd`. Plugins are entirely optional — removing a plugin folder removes the feature, with no code changes anywhere else.
|
||
|
||
### Bundled plugins
|
||
|
||
- **`canvas/`** — ASCII art canvas editor. Adds a toolbar button + ` ```ascii ` fenced block. Draw with brush, line, rectangle, ellipse, fill, and text tools using a monospace character grid.
|
||
- **`chat/`** — Encrypted chat between relay tenants. Footer flyout with delivery + read receipts.
|
||
- **`collab-buckets/`** — Collaborative buckets shared between relay tenants with exclusive card locking and E2E encrypted sync.
|
||
- **`email/`** — Email overlay (toggled with `M` key).
|
||
- **`gitea/`** — Gitea integration: create issues from cards, link to repos.
|
||
- **`slideshow/`** — Slideshow presentation mode for cards with `slide-image` blocks.
|
||
- **`logseq-import/`** — Import Logseq markdown journals and pages.
|
||
- **`tests/`** — UI + state test runner (40+ test files). Loaded only when `Ctrl+Shift+Alt+T` is pressed, so production users pay no cost for it.
|
||
|
||
### Plugin layout
|
||
|
||
```
|
||
public/plugins/myplugin/
|
||
├── plugin.json # Manifest (required)
|
||
├── myplugin.js # Entry point (loaded as ES module)
|
||
├── myplugin.css # Optional styles
|
||
├── myplugin.html # Optional HTML fragment (overlays, modals, etc.)
|
||
└── backend.php # Optional PHP hooks
|
||
```
|
||
|
||
**`plugin.json`:**
|
||
|
||
```json
|
||
{
|
||
"name": "myplugin",
|
||
"version": "1.0.0",
|
||
"description": "What this plugin does",
|
||
"frontend": {
|
||
"js": ["myplugin.js"],
|
||
"css": ["myplugin.css"],
|
||
"html": ["myplugin.html"]
|
||
},
|
||
"backend": "backend.php"
|
||
}
|
||
```
|
||
|
||
### Plugin API (`window.hyprcrd`)
|
||
|
||
Loaded synchronously before any module, so plugins can register handlers immediately:
|
||
|
||
| Namespace | Methods | Purpose |
|
||
|---|---|---|
|
||
| `hyprcrd.editor` | `registerCommand(name, fn)`, `getCommand(name)`, `api.{body, insertMd, updatePreview}` | Add toolbar commands. The `data-md="<name>"` button dispatches to the registered handler. `api` is set after the editor initializes. |
|
||
| `hyprcrd.markdown` | `registerFencedBlock(lang, fn)`, `registerPreProcessor(fn)`, `registerPostProcessor(fn)` | Add ` ```<lang> ` block renderers (returns HTML, passes through DOMPurify). Pre/post processors run on the source/HTML around `marked.parse()`. |
|
||
| `hyprcrd.hooks` | `on(name, fn)`, `off(name, fn)`, `emit(name, data)` | Generic event bus. Use for cross-plugin or core→plugin signaling. |
|
||
| `hyprcrd.util` | `$(idOrSel)`, `$$(sel)`, `esc(s)` | Lightweight DOM/escape helpers so plugins don't need to import core modules. |
|
||
| `hyprcrd.registerPlugin(meta)` | — | Register the plugin's metadata for introspection. |
|
||
|
||
### Minimal plugin example
|
||
|
||
`public/plugins/hello/plugin.json`:
|
||
|
||
```json
|
||
{
|
||
"name": "hello",
|
||
"version": "1.0.0",
|
||
"description": "Inserts a greeting",
|
||
"frontend": { "js": ["hello.js"] }
|
||
}
|
||
```
|
||
|
||
`public/plugins/hello/hello.js`:
|
||
|
||
```js
|
||
const h = window.hyprcrd;
|
||
|
||
h.editor.registerCommand('hello', () => {
|
||
h.editor.api.insertMd('Hello, world! ', '', '');
|
||
});
|
||
|
||
// Inject a toolbar button
|
||
const btn = document.createElement('button');
|
||
btn.dataset.md = 'hello';
|
||
btn.className = 'toolbar-btn';
|
||
btn.title = 'Hello';
|
||
btn.textContent = '👋';
|
||
document.getElementById('md-toolbar').appendChild(btn);
|
||
|
||
h.registerPlugin({ name: 'hello', version: '1.0.0' });
|
||
```
|
||
|
||
That's the entire plugin. Drop the folder in, reload, the button appears.
|
||
|
||
### Backend hooks (PHP)
|
||
|
||
If a plugin needs server-side code, set `"backend": "backend.php"` in the manifest. The file is `require_once`'d during bootstrap. Backend plugins can use `PluginManager::instance()->addToSlot('slot-name', '<html>')` to inject HTML into named slots in `index.php` (e.g. `md-toolbar`).
|
||
|
||
### Removing a plugin
|
||
|
||
```bash
|
||
rm -rf public/plugins/canvas
|
||
```
|
||
|
||
That's it. The toolbar button, overlay, CSS, and markdown handler all disappear together. The core has no awareness of any specific plugin.
|
||
|
||
## Data & Backup
|
||
|
||
All data lives in two places:
|
||
|
||
- `data/hyprcrd.db` — SQLite database (cards, buckets, settings)
|
||
- `public/uploads/` — uploaded images and audio
|
||
|
||
Back up both to preserve everything. The database auto-migrates on startup.
|
||
|
||
## Export Formats
|
||
|
||
- **JSON export** (`export` button) — full backup with media, re-importable
|
||
- **Markdown vault** (`vault` button) — Obsidian/Logseq-compatible structure:
|
||
```
|
||
Bucket Name/
|
||
Card Title.md # YAML frontmatter + markdown body
|
||
attachments/
|
||
image.jpg
|
||
```
|
||
|
||
## CalDAV Sync (ownCloud / Nextcloud)
|
||
|
||
hyprcrd can sync appointment cards with any CalDAV server (tested with ownCloud 10.15.3.0).
|
||
|
||
### Setup
|
||
|
||
1. Open the **calendar** (button in header)
|
||
2. Click the **gear icon** in the calendar header
|
||
3. Enter your CalDAV URL, username, and app password:
|
||
```
|
||
URL: https://cloud.example.com/remote.php/dav/calendars/your-user/personal/
|
||
Username: your-user
|
||
Password: your-app-password
|
||
```
|
||
4. Click **test** to verify the connection
|
||
5. Click **save**
|
||
|
||
### How it works
|
||
|
||
- Cards tagged with `#termin` are synced as calendar events
|
||
- The appointment card template maps to iCalendar fields:
|
||
```markdown
|
||
Title: 2026-04-15 — Team Meeting
|
||
|
||
**Zeit:** 14:30-15:30
|
||
**Ort:** Room 42
|
||
**Teilnehmer:** Alice, Bob
|
||
|
||
---
|
||
|
||
**Notizen:**
|
||
Agenda items here
|
||
|
||
#termin
|
||
```
|
||
- **Sync is bidirectional**: local changes push to the server, remote changes pull to hyprcrd
|
||
- **Conflict resolution**: last-modified timestamp wins
|
||
- **Deleted cards**: tracked via tombstones, deleted on the server on next sync
|
||
- **Auto-sync**: triggers when opening the calendar (max once per minute)
|
||
- **Manual sync**: click the cloud icon in the calendar header
|
||
|
||
### Security
|
||
|
||
- Credentials are encrypted at rest (AES-256-CBC) in the SQLite database
|
||
- All sync operations run server-side (PHP/cURL), no client-side credential exposure
|
||
- Supports HTTPS with certificate verification
|
||
|
||
## Calendar
|
||
|
||
The built-in calendar shows cards on their dates:
|
||
|
||
- **Journal cards** (title = `2026-04-08`) appear on that date
|
||
- **Appointment cards** (title = `2026-04-08 — Subject`) appear on their date
|
||
- **Other cards** fall back to their creation date
|
||
- **Click a day** to see all cards in a popup
|
||
- **Double-click** or use **+ journal** / **+ termin** to create new entries
|
||
- Appointment template includes Zeit, Ort, Teilnehmer, Notizen fields
|
||
- Navigate months with arrow buttons, **today** jumps to current date
|
||
|
||
## Vault Key (Multi-Instance Encryption)
|
||
|
||
When running multiple hyprcrd instances that share CalDAV credentials, you need a shared encryption key.
|
||
|
||
### Setup
|
||
|
||
1. Go to **Settings > Account > Vault Key**
|
||
2. Enter the same key (min 8 characters) on all instances
|
||
3. Click **save**
|
||
|
||
### How it works
|
||
|
||
- The vault key encrypts/decrypts CalDAV credentials stored in the database
|
||
- Key derivation: `SHA-256(vault_key)` → deterministic, same on all instances
|
||
- Without a vault key, encryption falls back to the login password hash (single-instance only)
|
||
- Changing the vault key automatically re-encrypts CalDAV credentials
|
||
- Changing the login password does NOT affect encryption when a vault key is set
|
||
|
||
### Important
|
||
|
||
- The vault key is stored in plaintext in each instance's database — it is NOT synced
|
||
- You must enter it manually on each connected instance
|
||
|
||
## Relay
|
||
|
||
A self-hosted **multi-tenant** message relay for hyprcrd. Multiple users
|
||
share one relay instance — each one has their own login, their own
|
||
client encryption secret, and their own inbox. Cards (and in the future:
|
||
encrypted chat messages) can be sent **between tenants** on the same
|
||
relay.
|
||
|
||
**Full documentation:** [`relay/README.md`](relay/README.md)
|
||
|
||
### Architecture
|
||
|
||
```
|
||
[hyprcrd alice] ──push──┐ ┌──poll── [hyprcrd alice]
|
||
├──► [Relay] ◄────────────┤
|
||
[hyprcrd bob] ──push──┘ tenants: alice, bob └──poll── [hyprcrd bob]
|
||
|
||
sender encrypts → relay decrypts with sender's secret →
|
||
re-encrypts with shared inter-tenant key → stores →
|
||
on recipient poll: decrypts → re-encrypts with recipient's secret
|
||
```
|
||
|
||
### Deploy
|
||
|
||
**Webspace** (any PHP host with `mod_rewrite` and `pdo_sqlite`):
|
||
|
||
```bash
|
||
cd relay
|
||
./dist.sh
|
||
# upload hyprcrd-relay.zip and extract into your webspace
|
||
```
|
||
|
||
**Docker**:
|
||
|
||
```bash
|
||
cd relay
|
||
docker build -t hyprcrd-relay .
|
||
docker run -d -p 8081:80 -v relay-data:/var/www/html/data hyprcrd-relay
|
||
```
|
||
|
||
The `data/` directory is created on first request and contains the
|
||
SQLite DB plus the master encryption key (chmod 600, blocked from web
|
||
access via `.htaccess`).
|
||
|
||
### Development (both together)
|
||
|
||
```bash
|
||
./dev.sh
|
||
```
|
||
|
||
Starts hyprcrd on `http://localhost:8088` and the relay on `http://localhost:8081` in the same Docker network. Source files are bind-mounted — changes are live without rebuilding. From inside the hyprcrd container, the relay is reachable at `http://relay:80`.
|
||
|
||
Under the hood this runs `docker compose --profile relay up --build`. The relay service lives behind the `relay` compose profile, so a plain `docker compose up` brings up only the app — handy when you're developing against a remote relay.
|
||
|
||
### Setup workflow
|
||
|
||
1. Open the relay URL **from a desktop browser** — the setup wizard
|
||
is desktop-friendly
|
||
2. Create the first **admin** account: username + password (UI login)
|
||
+ shared secret (client encryption)
|
||
3. As admin, click the **users** tab and create accounts for any
|
||
additional users you want to give relay access to
|
||
4. Each user enters their credentials in their own hyprcrd:
|
||
**Settings → Relay** → relay URL + username + shared secret
|
||
5. To send a card from one tenant to another, use the relay UI's
|
||
compose form (recipient picker) — or push cards from hyprcrd
|
||
|
||
### How it works (high level)
|
||
|
||
- **Per-tenant inboxes**: each user only sees their own messages
|
||
- **Cross-tenant routing**: send a card to another tenant on the same
|
||
relay; the relay decrypts with the sender's secret, re-encrypts with
|
||
a shared inter-tenant key for storage, then re-encrypts with the
|
||
recipient's secret on delivery
|
||
- **At-rest encryption**: every sensitive value in the DB (tenant
|
||
secrets, the inter-tenant key, message bodies) is encrypted. The
|
||
master key lives outside the DB in a chmod-600 file
|
||
- **24-hour ephemeral**: messages and attachments auto-delete after a
|
||
day. Cards extracted in hyprcrd are the long-term store
|
||
- **Encrypted chat**: tenant-to-tenant chat with delivery + read receipts
|
||
(via chat plugin). Per-pair E2E key exchange is on the roadmap
|
||
|
||
### Relay Security
|
||
|
||
- Per-tenant bcrypt-hashed passwords + secrets
|
||
- Master key encryption for tenant secrets and the inter-tenant key
|
||
at rest
|
||
- HttpOnly + SameSite=Strict session cookies
|
||
- Rate-limited login (5 attempts/minute per IP)
|
||
- AES-256-CBC for all message encryption
|
||
- Security headers (X-Frame-Options, HSTS, X-Content-Type-Options)
|
||
- `data/` directory blocked from direct download via `.htaccess`
|
||
- Request size limited to 256KB (15MB for attachments)
|
||
- Last-admin protection prevents accidental lockout
|
||
|
||
For the full security model (including what's NOT protected and the
|
||
roadmap to strict E2E for chat), see [`relay/README.md`](relay/README.md).
|
||
|
||
### Migrating from the old single-tenant relay
|
||
|
||
When you upgrade an existing relay installation, the new code automatically
|
||
detects the legacy schema and drops the old `messages` and `attachments`
|
||
tables (the data is 24h-ephemeral anyway). The setup wizard appears on
|
||
your next visit. You'll need to:
|
||
1. Run through the desktop setup wizard to create the first admin
|
||
2. Recreate any other tenant accounts that need access
|
||
3. On every connected hyprcrd, update **Settings → Relay** to add the
|
||
new **username** field and the new shared secret
|
||
|
||
The old `relay_key` was bcrypt-hashed and cannot be recovered, so all
|
||
client secrets need to be reissued.
|
||
|
||
## Electron Desktop App
|
||
|
||
hyprcrd can run as a standalone desktop app via Electron — no web server needed.
|
||
|
||
```bash
|
||
cd electron
|
||
./fetch-php.sh # download static PHP binary (~20MB)
|
||
npm install
|
||
npm start # dev mode
|
||
npm run dist # build AppImage / DMG / NSIS
|
||
```
|
||
|
||
The app bundles a static PHP binary per platform and stores user data (database + uploads) outside the app bundle:
|
||
|
||
- **Linux**: `~/.config/hyprcrd/data/`
|
||
- **macOS**: `~/Library/Application Support/hyprcrd/data/`
|
||
- **Windows**: `%APPDATA%/hyprcrd/data/`
|
||
|
||
See [`electron/README.md`](electron/README.md) for details.
|
||
|
||
## Rebuilding Tailwind CSS
|
||
|
||
After changing Tailwind utility classes in HTML or JS files:
|
||
|
||
```bash
|
||
# Download standalone CLI (once)
|
||
curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 -o tailwindcss && chmod +x tailwindcss
|
||
|
||
# Build (includes plugins so plugin HTML/JS contributes utility classes)
|
||
./tailwindcss --input public/css/tailwind.css --output public/css/tailwind.out.css --content "public/index.php,public/js/**/*.js,public/plugins/**/*.{js,html}" --minify
|
||
```
|
||
|
||
## Security
|
||
|
||
- Bcrypt password hashing
|
||
- CSRF protection on all mutations
|
||
- Rate-limited login (5 attempts/60s)
|
||
- Upload directory blocks PHP execution
|
||
- DOMPurify sanitizes all rendered markdown
|
||
- Security headers (X-Frame-Options, HSTS, etc.)
|
||
- AES-256-CBC encryption for CalDAV credentials and Relay messages
|
||
- Vault key system for multi-instance CalDAV encryption (independent of login password)
|
||
- Relay: separate password + shared secret, rate-limited, client-side encryption
|
||
|
||
## License
|
||
|
||
MIT
|