#!/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())