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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,25 @@ DB_SESSION_TTL_HOURS=168
# "1" or "true" = add the Secure flag to the session cookie.
# Enable when serving over HTTPS (Coolify TLS proxy).
DB_SECURE_COOKIE=false

# ── API-key storage (optional encryption at rest) ────────────────────────────
# Empty (default): keys.json stays PLAINTEXT (mode 0600) β€” backward compatible.
# Set: keys.json is AES-256-GCM encrypted; the key is derived from this
# passphrase via Argon2id. Existing plaintext files migrate on first access.
#
# This passphrase protects the file AT REST only β€” the real API key still
# flows to the provider's Authorization header per request. Treat it like a
# password: keep it in your secret manager, set it from FIRST BOOT, and never
# lose it (an encrypted keys.json without the passphrase is unreadable, and
# writes are refused so it is never silently overwritten). Generate with:
# openssl rand -hex 32
DB_KEYS_PASSPHRASE=

# ── Custom OpenAI-compatible provider (optional env override) ─────────────────
# Set these to provision/override the "openai-compatible" custom provider from
# the environment. When DB_OPENAI_COMPAT_BASE_URL is set, the UI shows the custom
# endpoint as READ-ONLY ("from env" badge) and the backend uses these values.
# Safe backward compat: unset β†’ keys.json / in-app Settings behavior as before.
# DB_OPENAI_COMPAT_BASE_URL=https://proxy.example.com/v1
# DB_OPENAI_COMPAT_API_KEY=sk-...
# DB_OPENAI_COMPAT_MODEL=gpt-oss-20b
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ env:
CARGO_TERM_COLORS: always
NODE_VERSION: '22'

permissions:
contents: read

jobs:
lint:
runs-on: macos-14
Expand All @@ -27,6 +30,8 @@ jobs:
run: npx oxlint frontend/
- name: TypeScript type check
run: npx tsc -b
- name: ACL guard (every Tauri command has an allow entry)
run: node test/check-acl.mjs
- name: Frontend tests
run: npx vitest run
- name: Check version consistency (manifests + locks + changelog section)
Expand Down Expand Up @@ -56,6 +61,53 @@ jobs:
run: cargo test --manifest-path server/Cargo.toml
- name: Cargo clippy
run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
e2e:
runs-on: macos-14
needs: [test]
# Heavy matrix job: PRs wait for the same approval gate as the build job;
# push to master / tags / manual run without approval (behavior unchanged).
environment:
name: ${{ github.event_name == 'pull_request' && 'build-approval' || 'release-auto' }}
strategy:
matrix:
browser: [chromium, webkit]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium webkit
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Build web server (debug β€” used by the e2e harness)
run: cargo build --manifest-path server/Cargo.toml
- name: Build frontend
run: npm run build
- name: "E2E β€” all suites (web-smoke, trash, theme, AI transport)"
run: npm run test:e2e
env:
BROWSER: ${{ matrix.browser }}
- name: Upload e2e logs (server + browser console)
if: always()
uses: actions/upload-artifact@v7
with:
name: e2e-logs-${{ matrix.browser }}
path: test/artifacts/
if-no-files-found: ignore

test-server-linux:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
# Server-only on Linux: exercises cfg(target_os="linux") paths (delete β†’
# .trash server-side trash) that never run on the macos-14 test job.
runs-on: ubuntu-22.04
needs: [lint]
steps:
- uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Cargo test (web server, Linux)
run: cargo test --manifest-path server/Cargo.toml

security:
runs-on: ubuntu-22.04
needs: [lint]
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ src-tauri/target/
.env
.env.*
!.env.example
frontend/e2e/screenshot/
test/artifacts/
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
# Changelog

## v0.1.0-rc.1 β€” 2026-08-08

### Release candidate β€” AI transport hardening + web/Docker production readiness

#### πŸš€ Features
- **Vault Q&A / generation routing** β€” `[[wikilink]]`, questions, and generation verbs on an empty doc answer FROM vault context (no tools, plain Markdown insert); `isVaultGenerationIntent` + `buildVaultGroundingPrompt`
- **Custom OpenAI-compatible provider via env** β€” `DB_OPENAI_COMPAT_BASE_URL`/`_API_KEY`/`_MODEL` provision the custom endpoint headless; the UI shows it read-only ("from env" badge). Safe backward compat: unset = in-app behavior
- **Server-side trash (web)** β€” deleted files move to `.trash/` inside the vault (persistent in `/data`), excluded from tree/search/git; sidebar Trash panel with restore + empty
- **Clickable `[[wikilink]]`** β€” accent + underline visual, hover hint, single-click tooltip with Open action, Cmd/Ctrl+Click navigates (Obsidian-style); merged the wikilink search into the ⌘K link popover (one icon)
- **Persistent sessions** β€” `sessions.json` (SHA-256 hashed tokens) survives server restarts; no more forced re-login after redeploy
- **Optional keys.json encryption** β€” `DB_KEYS_PASSPHRASE` β†’ AES-256-GCM at rest (Argon2id KDF); plaintext migrates on first access, encrypted files never overwritten without the passphrase
- **Consent-gated open access** β€” setup wizard "Skip" requires acknowledging that anyone with the URL can access

#### πŸ› Bug Fixes
- **AI transport**: probe per provider+model (not per provider); ops-only output channel (no text+ops double-write); removed Path A→B retry (text-only models no longer pay 2× generation); `crypto.randomUUID` secure-context fallback (`uuid()`)
- **Web server**: `test_connection` camelCase args fix (was 400 + key never used); tool probe `tool_choice:"required"` + JSON `tools:false` contract
- **Shortcuts**: βŒ˜β‡§F/βŒ˜β‡§P no longer hijack search; canonical βŒ˜β‡§F/⌘βŒ₯⇧F new file/folder + native ⌘N alias
- **Wiki**: recursive scan + content search for note-linking; `read_file` completes extension-less references to `.md` (never double-appends)

#### πŸ§ͺ Testing & CI
- **E2E suite** β€” web-smoke, trash, theme-check, ai-debug (Path A + Path B) via one `npm run test:e2e`; CI matrix `[chromium, webkit]` with PR approval gate; logs (not screenshots) as artifacts
- **ACL guard** β€” CI fails if a Tauri command lacks its `allow-*` entry
- **test-server-linux** job β€” exercises `cfg(target_os="linux")` trash paths
- Rust tests 51+, frontend 64+, e2e 39+ assertions

## v0.1.0-beta.4 β€” 2026-08-07

### Docker /data self-heal + server boot diagnostics
Expand Down
94 changes: 67 additions & 27 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,27 +77,46 @@ frontend/
PasswordInput.tsx Password field with show/hide toggle (login + change-password)
AppearanceSettings.tsx Theme picker (named themes: Midnight / Bright Surfaces)
ShortcutsModal.tsx Keyboard shortcuts reference
OnboardingGuide.tsx First-run guide for new vaults
ErrorBoundary.tsx Root crash recovery screen
stores/
editor.ts Tabs, file content, edited content, undo/redo state
vault.ts Vault state, tree, folder expansion, recent vaults
aiSettings.ts Provider/model/saved-providers (persisted) β€” API keys live only in keychain
aiSettings.ts Provider/model/saved-providers (persisted) β€” API keys
live only backend-side (Keychain / keys.json, never the webview)
auth.ts Web auth status (setup β†’ login β†’ ready), 401 handling
gitStatus.ts Shared git status (branch + porcelain) β€” one poller
theme.ts Named theme store (data-theme + Tauri window + meta theme-color)
data/
providers.ts Auto-generated provider/model catalog (models.dev)
providers.ts Generated provider/model catalog β€” regenerate with
`node frontend/data/fetch-providers.mjs` (models.dev api.json)
fetch-providers.mjs Catalog generator script (fetches models.dev, writes providers.ts)
hooks/
useKeyboard.ts Keyboard shortcut handling
usePolling.ts Interval polling
useClickOutside.ts Click-outside detection for menus
utils/
aiBlocks.ts AI text β†’ applyDocumentOperations (suggestions)
aiBlocks.ts Markdown β†’ applyDocumentOperations; normalization, semantic
validation, prompt builders (edit/vault-first), vault-intent routing
setupWizard.ts Pure setup-wizard validation + payload builder (unit-testable)
e2e/
theme-check.mjs Playwright theme E2E (dark/light switch + picker)
web-smoke.mjs Playwright full-stack smoke (setup β†’ login)
screenshot/ E2E screenshots (gitignored)
iteratorPolyfill.ts Safari ES2023 iterator polyfills
uuid.ts Secure-context-safe UUID v4 (crypto.randomUUID with
getRandomValues/Math.random fallback β€” plain-HTTP/IP access)
test/
lib.mjs Shared CI-friendly harness: server + browser logs to
artifacts/, browser engine resolution (chromium/webkit,
system-Chrome fallback), pass/fail summary
run-all.mjs One entry point for all suites (npm run test:e2e;
BROWSER env picks the engine)
web-smoke.mjs Full-stack smoke: setup wizard β†’ login β†’ persistent
session across server restart
trash.mjs Trash UI: empty state (disabled) β†’ restore β†’ back in tree
theme-check.mjs Theme E2E (dark/light switch + picker in Settings)
ai-debug.mjs AI transport e2e (mock provider: Path A tools + Path B
text-only, selection + markdown)
check-acl.mjs ACL guard: every Tauri command has an allow-* entry
(run in CI lint)
artifacts/ Run logs (server + browser console) + results (gitignored)
src-tauri/
Cargo.toml Desktop crate (bin docubook-desktop + lib docubook)
tauri.conf.json Window config (theme: Dark), CSP, bundle
Expand All @@ -117,23 +136,26 @@ server/ Web distribution β€” standalone axum crate (no Tauri)
Cargo.toml Bin docubook-server (musl-friendly, [[bin]] path = main.rs)
main.rs HTTP server: /api/<cmd> dispatcher, SSE AI streaming,
auth middleware, static file serving (SPA fallback)
auth.rs Argon2id passwords, in-memory sessions, login rate limit
auth.rs Argon2id passwords, persistent sessions (sessions.json,
SHA-256 hashed tokens, survive restarts), login rate limit
config.rs Config merge (env > /data/config.json > default)
keys.rs API-key store (keys.json, 0600)
keys.rs API-key store (keys.json, 0600; optional AES-256-GCM
encryption at rest via DB_KEYS_PASSPHRASE, Argon2id KDF)
dist/ Frontend build output (gitignored; served by server + Tauri)
public/ Static assets (appicon.png)
patches/ patch-package patches for node_modules
Dockerfile Multi-stage web image (node β†’ rust musl β†’ alpine)
docker-compose.yml Web deployment (volume /data, env reference)
docker-entrypoint.sh Container entrypoint (data-dir self-heal + boot diagnostics)
rust-toolchain.toml Pinned Rust toolchain (build reproducibility, REL-2)
.env.example All server environment variables
```

## Architecture Notes

- **Trust boundary:** the Rust backend (desktop `src-tauri` / web `server`) is trusted; the frontend is not. File paths are canonicalized against the vault root, AI base URLs are allowlisted (SSRF guard), and API keys never reach the frontend β€” the webview cannot read them.
- **Two runtimes, one frontend:** `frontend/lib/ipc.ts` abstracts Tauri IPC and HTTP/SSE behind one `invoke`/`listen` API, so components are runtime-agnostic. The web server reuses the desktop app's pure modules (`vault`, `wiki`, `git`, `search`, `agent`) via `#[path]` includes β€” never edit them in one place only.
- **Web auth:** first run creates an admin account (Argon2id); sessions are httpOnly cookies (rate-limited login). `DB_NO_AUTH=1` keeps open access (pre-web behavior). Env vars win over the Settings β†’ System overrides.
- **API keys:** macOS Keychain on desktop; `keys.json` (0600) in `/data` on web. Both are resolved server-side in `ask_ai` β€” a frontend-supplied key is ignored.
- **Web auth:** first run creates an admin account (Argon2id); sessions are httpOnly cookies (rate-limited login) persisted in `sessions.json` (SHA-256 hashed tokens β€” they survive server restarts, so redeploys don't log users out). `DB_NO_AUTH=1` keeps open access (pre-web behavior); the setup wizard's "Skip β€” keep open access" is consent-gated (acknowledgement checkbox). Env vars win over the Settings β†’ System overrides.
- **API keys:** macOS Keychain on desktop; `keys.json` (0600) in `/data` on web, optionally AES-256-GCM encrypted at rest via the `DB_KEYS_PASSPHRASE` env var (Argon2id-derived key; plaintext files auto-migrate, encrypted files are never overwritten without the passphrase). Both are resolved server-side in `ask_ai` β€” a frontend-supplied key is ignored.
- **Permissions (desktop):** if you add or remove a Tauri command, regenerate `src-tauri/permissions/default.toml` and `src-tauri/capabilities/default.json` in the same change (see the header comment in the permission file).
- **Versions:** `package.json`, `src-tauri/Cargo.toml`, and `src-tauri/tauri.conf.json` must stay in sync β€” CI enforces it. `server/Cargo.toml` is versioned independently.

Expand All @@ -147,6 +169,22 @@ docker-compose.yml Web deployment (volume /data, env reference)
- `npm run build`
- `cd src-tauri && cargo test`
- `cd server && cargo test`
- Build the web server + frontend, then the Playwright suites
(run logs land in `test/artifacts/` β€” server stdout/stderr,
browser console, and per-run results):
`cargo build --manifest-path server/Cargo.toml && npm run build`
`npm run test:e2e` # all suites, chromium (default)
`BROWSER=webkit npm run test:e2e` # webkit β€” CI only (macos-14 runner)

Note β€” environment matrix (no ambiguity):
- macOS 12 (dev machine): the app minimum is macOS 12, validated by daily
development on it. Playwright 1.62+ ships mac14-only browser builds, so
the local chromium e2e uses the system Chrome fallback; webkit runs are
CI-only.
- macOS 14 (CI, macos-14 runner): full e2e matrix (chromium + webkit) with
the pinned Playwright builds β€” behavior validation on a newer supported
OS. A passing CI run is a superset check, not a claim about macOS 12
internals; the minimum-OS claim rests on the dev machine itself.
4. Open a PR against `master` using the PR template.

### Commit conventions (enforced by the commit-msg hook)
Expand All @@ -155,22 +193,24 @@ docker-compose.yml Web deployment (volume /data, env reference)
<type>(<scope>): <subject>
```

| Type | Usage |
|------|-------|
| `feat` | new feature |
| `fix` | bug fix |
| `chore` | maintenance (release, deps) |
| `ci` | CI / pipeline |
| `docs` | documentation (README, CONTRIBUTING, CHANGELOG) |
| `perf` | performance optimization |
| `refactor` | structural change without behavior change |
| `test` | test suite / test tooling |
| `security` | security hardening / audit |

- **Scope** is optional, kebab-case: `fix(docker):`, `ci(release):`, `feat(theme):`
- **Subject**: concise, imperative, lowercase β€” add a body for the WHY when needed
**DRY mapping β€” the commit subject IS the changelog line.** Each type maps 1:1 to a CHANGELOG category; a release section is assembled by grouping the merged PR subjects by type (no rewriting):

| Type | CHANGELOG category | Usage |
|------|--------------------|-------|
| `feat` | πŸš€ Features | new feature |
| `fix` | πŸ› Bug Fixes | bug fix |
| `security` | πŸ›‘οΈ Security | security hardening / audit |
| `perf` | ⚑ Performance | performance optimization |
| `refactor` | πŸ”„ Refactor | structural change without behavior change |
| `docs` | πŸ“š Documentation | documentation (README, CONTRIBUTING) |
| `test` | πŸ§ͺ Testing & CI | test suite / test tooling |
| `ci` | πŸ”§ CI | CI / pipeline |
| `chore` | πŸ”„ Version / Hygiene | maintenance (release, deps) |

- **Scope** is optional, kebab-case: `fix(docker):`, `ci(release):`, `feat(theme):` β€” when it adds signal, keep it as a prefix on the changelog bullet (`feat(theme):` β†’ "theme: …")
- **Subject**: concise, imperative, lowercase β€” write it as the changelog line it will become
- **PR merge commits** (squash) are exempt from the hook
- Commit messages are NOT used for auto-changelog (CHANGELOG.md is manual) β€” the convention keeps history readable
- **Release changelog = the merged PR subjects grouped by type** β€” each subject lands verbatim under its category in `CHANGELOG.md`; the section is assembled from commits, not rewritten (DRY)
- The hook rejects other formats and lists the allowed types β€” no commitlint needed

**CI runs the full artifact matrix on every PR** (not just on release): frontend build, desktop DMG, web server binary, and a full `docker build` of the web image (which also reports the image size). If your change touches the Dockerfile, the Rust modules, or the frontend, the PR build is the fastest way to catch breakage.
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ COPY --from=server /src/server/target/release/docubook-server /app/docubook-serv
COPY --from=web /app/dist /app/www
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Runtime config (DB_*) is passed via compose/run/panel β€” only static
# defaults live here; the full variable list is in .env.example.
ENV DATA_DIR=/data WWW_DIR=/app/www PORT=8080
# /data must exist with docubook ownership BEFORE first start: named volumes
# inherit the mount-point ownership, so without this the volume is root-owned
Expand Down
Loading