feat: add Obsidian CLI API service
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
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:
@@ -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())
|
||||
Reference in New Issue
Block a user