feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s

- Add Docker image for the official Obsidian desktop app and CLI.

- Start Obsidian headlessly with Xvfb and DBus setup.

- Expose a safe structured HTTP command API without shell execution.

- Add JWT-based vault, path, and command authorization.

- Support single-vault and multi-vault container mounts.

- Add TypeScript SDK helpers for Obsidian CLI commands.

- Add n8n community node package with Obsidian operations.

- Add docs, compose config, tests, and production image workflow.
This commit is contained in:
2026-06-24 09:11:13 +02:00
commit 902a3df8b5
34 changed files with 5153 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Create an HS256 JWT for the Obsidian API.
Example:
OBSIDIAN_JWT_SECRET=change-this-long-random-secret \
python scripts/create_jwt.py \
--sub n8n \
--vault work \
--path Inbox/n8n/ \
--command create --command read --command append \
--days 365
"""
from __future__ import annotations
import argparse, hashlib, base64
import json, hmac, time, os
from typing import Any
def b64url(value: bytes) -> str:
return base64.urlsafe_b64encode(value).decode().rstrip("=")
def sign_jwt(claims: dict[str, Any], secret: bytes) -> str:
header = {"alg": "HS256", "typ": "JWT"}
header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode())
payload_b64 = b64url(json.dumps(claims, separators=(",", ":")).encode())
signing_input = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(secret, signing_input, hashlib.sha256).digest()
return f"{header_b64}.{payload_b64}.{b64url(signature)}"
def main() -> int:
parser = argparse.ArgumentParser(description="Create an HS256 JWT for obsidian-api")
parser.add_argument("--secret", help="JWT signing secret. Defaults to OBSIDIAN_JWT_SECRET env.")
parser.add_argument("--sub", required=True, help="Subject/name for the token, e.g. n8n")
parser.add_argument("--name", help="Display name claim")
parser.add_argument("--vault", action="append", dest="vaults", default=[], help="Allowed vault. Repeatable. Default: *")
parser.add_argument("--path", action="append", dest="paths", default=[], help="Allowed path/prefix. Repeatable. Default: *")
parser.add_argument("--command", action="append", dest="commands", default=[], help="Allowed CLI command. Repeatable. Default: *")
parser.add_argument("--days", type=int, default=365, help="Days until expiry")
parser.add_argument("--claim", action="append", default=[], help="Extra claim as key=value. Repeatable.")
args = parser.parse_args()
secret = args.secret or os.getenv("OBSIDIAN_JWT_SECRET", "")
if not secret:
raise SystemExit("Missing JWT secret. Set OBSIDIAN_JWT_SECRET or pass --secret.")
now = int(time.time())
claims: dict[str, Any] = {
"sub": args.sub,
"iat": now,
"exp": now + args.days * 24 * 60 * 60,
"vaults": args.vaults or ["*"],
"paths": args.paths or ["*"],
"commands": args.commands or ["*"],
}
if args.name:
claims["name"] = args.name
for item in args.claim:
if "=" not in item:
raise SystemExit(f"Invalid --claim {item!r}; expected key=value")
key, value = item.split("=", 1)
claims[key] = value
print(sign_jwt(claims, secret.encode()))
return 0
if __name__ == "__main__":
raise SystemExit(main())