Release cookie-free Persimmons vault link 0.6.0

This commit is contained in:
matcha committed 2026-10-04 08:05:18 +00:00
1 parent 53c4bb50f4
commit 1aa3cffaf2
32 files changed
+1535 -624

No files matched your search

+87 -16
View File
@@ -6,10 +6,14 @@ import binascii
import json
import os
import re
import secrets
import hashlib
import time
import sqlite3
from urllib.parse import urlsplit
from contextlib import closing
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route
@@ -17,6 +21,13 @@ EXTENSION_ORIGIN = "chrome-extension://kiolgjoohhgdohcafgelhikpgkbmckbg"
PREFIX = "/chat/persimmons/human-vault/items"
MAX_BODY = 24 * 1024
ITEM_ID = re.compile(r"[0-9a-f]{32}")
CAPABILITY = re.compile(r"[A-Za-z0-9_-]{40,128}")
CAPABILITY_TTL = 30 * 24 * 60 * 60
DEFAULT_DB = "/data/human-vault.sqlite"
def storage_path():
return os.environ.get("PERSIMMONS_HUMAN_VAULT_DB", DEFAULT_DB)
def validate_item(item):
@@ -53,6 +64,29 @@ class Store:
def __init__(self, path):
self.path = path
def _cap_table(self, db):
db.execute("CREATE TABLE IF NOT EXISTS human_capabilities (owner TEXT NOT NULL, token_hash TEXT NOT NULL PRIMARY KEY, expires_at INTEGER NOT NULL)")
db.execute("DELETE FROM human_capabilities WHERE expires_at < ?", (int(time.time()),))
def mint_capability(self, owner):
token = secrets.token_urlsafe(32)
digest = hashlib.sha256(token.encode()).hexdigest()
expires_at = int(time.time()) + CAPABILITY_TTL
with closing(sqlite3.connect(self.path, timeout=5)) as db, db:
self._cap_table(db)
db.execute("DELETE FROM human_capabilities WHERE owner=?", (owner,))
db.execute("INSERT INTO human_capabilities (owner,token_hash,expires_at) VALUES (?,?,?)", (owner, digest, expires_at))
return token, expires_at
def capability_owner(self, token):
if not isinstance(token, str) or not CAPABILITY.fullmatch(token):
return None
digest = hashlib.sha256(token.encode()).hexdigest()
with closing(sqlite3.connect(self.path, timeout=5)) as db, db:
self._cap_table(db)
row = db.execute("SELECT owner FROM human_capabilities WHERE token_hash=? AND expires_at>=?", (digest, int(time.time()))).fetchone()
return row[0] if row else None
def operate(self, owner, method, item_id=None, item=None):
with closing(sqlite3.connect(self.path, timeout=5)) as db, db:
db.execute("CREATE TABLE IF NOT EXISTS human_items (owner TEXT NOT NULL, id TEXT NOT NULL, origin TEXT NOT NULL, iv TEXT NOT NULL, ciphertext TEXT NOT NULL, username TEXT, PRIMARY KEY(owner,id))")
@@ -102,18 +136,23 @@ def routes(resolve_actor):
return reply({"error": "query forbidden"}, 400)
if request.method == "OPTIONS":
requested = {h.strip().lower() for h in request.headers.get("access-control-request-headers", "").split(",") if h.strip()}
if request.headers.get("access-control-request-method") not in {"GET", "POST", "PUT", "DELETE"} or not requested <= {"content-type", "x-vault-owner"}:
if request.headers.get("access-control-request-method") not in {"GET", "POST", "PUT", "DELETE"} or not requested <= {"content-type", "x-vault-owner", "x-vault-capability"}:
return reply({"error": "forbidden"}, 403)
return Response(status_code=204, headers={**headers, "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", "Access-Control-Allow-Headers": "Content-Type, X-Vault-Owner"})
if not request.headers.get("cookie") or request.headers.get("authorization"):
return reply({"error": "unauthorized"}, 401)
try:
actor = await resolve_actor(request)
except Exception:
return reply({"error": "session unavailable"}, 503)
if not actor:
return reply({"error": "unauthorized"}, 401)
owner = actor.lower()
return Response(status_code=204, headers={**headers, "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", "Access-Control-Allow-Headers": "Content-Type, X-Vault-Owner, X-Vault-Capability"})
path = storage_path()
if not path:
return reply({"error": "vault not configured"}, 503)
owner = Store(path).capability_owner(request.headers.get("x-vault-capability"))
if not owner:
if not request.headers.get("cookie") or request.headers.get("authorization"):
return reply({"error": "unauthorized"}, 401)
try:
actor = await resolve_actor(request)
except Exception:
return reply({"error": "session unavailable"}, 503)
if not actor:
return reply({"error": "unauthorized"}, 401)
owner = actor.lower()
item_id = request.path_params.get("item_id")
if item_id is not None and not ITEM_ID.fullmatch(item_id):
return reply({"error": "not found"}, 404)
@@ -134,14 +173,46 @@ def routes(resolve_actor):
raise ValueError()
except (ValueError, TypeError, binascii.Error):
return reply({"error": "invalid encrypted item"}, 400)
path = os.environ.get("PERSIMMONS_HUMAN_VAULT_DB")
if not path:
return reply({"error": "vault not configured"}, 503)
try:
payload, code = Store(path).operate(owner, request.method, item_id, item)
except (sqlite3.Error, OSError):
return reply({"error": "vault unavailable"}, 503)
return reply(payload, code)
return [Route(PREFIX, endpoint, methods=["GET", "POST", "OPTIONS"]),
Route(PREFIX + "/{item_id}", endpoint, methods=["GET", "PUT", "DELETE", "OPTIONS"])]
async def read_endpoint(request):
if request.method == "OPTIONS":
return await endpoint(request)
async for chunk in request.stream():
if chunk:
return JSONResponse({"error": "read body must be empty"}, status_code=400)
read_request = Request({**request.scope, "method": "GET"}, receive=request.receive)
return await endpoint(read_request)
async def link_endpoint(request):
headers = {"Cache-Control": "no-store"}
if request.url.scheme != "https" or request.headers.get("origin") != "https://persimmons.app":
return JSONResponse({"error": "forbidden"}, status_code=403, headers=headers)
if request.method != "POST" or request.headers.get("authorization") or not request.headers.get("cookie") or request.headers.get("x-requested-with") != "Persimmons":
return JSONResponse({"error": "unauthorized"}, status_code=401, headers=headers)
try:
actor = await resolve_actor(request)
except Exception:
return JSONResponse({"error": "session unavailable"}, status_code=503, headers=headers)
if not actor:
return JSONResponse({"error": "unauthorized"}, status_code=401, headers=headers)
path = storage_path()
if not path:
return JSONResponse({"error": "vault not configured"}, status_code=503, headers=headers)
try:
token, expires_at = Store(path).mint_capability(actor.lower())
except (sqlite3.Error, OSError):
return JSONResponse({"error": "vault unavailable"}, status_code=503, headers=headers)
return JSONResponse({"owner": actor.lower(), "capability": token, "expires_at": expires_at}, headers=headers)
return [
Route(PREFIX, endpoint, methods=["GET", "POST", "OPTIONS"]),
Route(PREFIX + "/{item_id}", endpoint, methods=["GET", "PUT", "DELETE", "OPTIONS"]),
Route(PREFIX + "/read", read_endpoint, methods=["POST", "OPTIONS"]),
Route(PREFIX + "/{item_id}/read", read_endpoint, methods=["POST", "OPTIONS"]),
Route("/chat/persimmons/human-vault/link", link_endpoint, methods=["POST"]),
]