Initial scaffold for jirorian
uv project with FastAPI + boto3 + pynostr, wired to uv2nix flake. Stub modules for NIP-98 auth, NIP-96 upload endpoint, R2 presigned URLs, Nostr relay subscription, and SQLite whitelist/rate-limit store. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
a56195620a
10 changed files with 1563 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.python-version
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
.env
|
||||||
97
flake.nix
Normal file
97
flake.nix
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
description = "jirorian - Nostr-native R2 image upload API";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
pyproject-nix = {
|
||||||
|
url = "github:pyproject-nix/pyproject.nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
uv2nix = {
|
||||||
|
url = "github:pyproject-nix/uv2nix";
|
||||||
|
inputs.pyproject-nix.follows = "pyproject-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
pyproject-build-systems = {
|
||||||
|
url = "github:pyproject-nix/build-system-pkgs";
|
||||||
|
inputs.pyproject-nix.follows = "pyproject-nix";
|
||||||
|
inputs.uv2nix.follows = "uv2nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
pyproject-nix,
|
||||||
|
uv2nix,
|
||||||
|
pyproject-build-systems,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
inherit (nixpkgs) lib;
|
||||||
|
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
||||||
|
|
||||||
|
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
|
||||||
|
|
||||||
|
overlay = workspace.mkPyprojectOverlay {
|
||||||
|
sourcePreference = "wheel";
|
||||||
|
};
|
||||||
|
|
||||||
|
editableOverlay = workspace.mkEditablePyprojectOverlay {
|
||||||
|
root = "$REPO_ROOT";
|
||||||
|
};
|
||||||
|
|
||||||
|
pythonSets = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
python = pkgs.python3;
|
||||||
|
in
|
||||||
|
(pkgs.callPackage pyproject-nix.build.packages {
|
||||||
|
inherit python;
|
||||||
|
}).overrideScope
|
||||||
|
(
|
||||||
|
lib.composeManyExtensions [
|
||||||
|
pyproject-build-systems.overlays.wheel
|
||||||
|
overlay
|
||||||
|
]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells = forAllSystems (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
pythonSet = pythonSets.${system}.overrideScope editableOverlay;
|
||||||
|
virtualenv = pythonSet.mkVirtualEnv "jirorian-dev-env" workspace.deps.all;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
virtualenv
|
||||||
|
pkgs.uv
|
||||||
|
];
|
||||||
|
env = {
|
||||||
|
UV_NO_SYNC = "1";
|
||||||
|
UV_PYTHON = pythonSet.python.interpreter;
|
||||||
|
UV_PYTHON_DOWNLOADS = "never";
|
||||||
|
};
|
||||||
|
shellHook = ''
|
||||||
|
unset PYTHONPATH
|
||||||
|
export REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
packages = forAllSystems (system: {
|
||||||
|
default = pythonSets.${system}.mkVirtualEnv "jirorian-env" workspace.deps.default;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
0
jirorian/__init__.py
Normal file
0
jirorian/__init__.py
Normal file
12
jirorian/auth.py
Normal file
12
jirorian/auth.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# NIP-98 HTTP Auth verification
|
||||||
|
# Verifies kind:27235 Nostr events used as Bearer tokens.
|
||||||
|
# Rejects events older than MAX_AGE_SECONDS to prevent replay attacks.
|
||||||
|
|
||||||
|
MAX_AGE_SECONDS = 60
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_nip98(authorization: str, expected_url: str, expected_method: str) -> str:
|
||||||
|
# TODO: base64-decode the event, verify secp256k1 signature via pynostr,
|
||||||
|
# check created_at freshness, url and method tags match request.
|
||||||
|
# Returns the verified pubkey (hex) on success, raises HTTPException on failure.
|
||||||
|
raise NotImplementedError
|
||||||
20
jirorian/main.py
Normal file
20
jirorian/main.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI(title="jirorian")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/.well-known/nostr/nip96.json")
|
||||||
|
async def nip96_info():
|
||||||
|
# TODO: return NIP-96 server info document
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/upload/request")
|
||||||
|
async def upload_request():
|
||||||
|
# TODO: verify NIP-98 auth, check whitelist, return presigned R2 URL
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("jirorian.main:app", host="127.0.0.1", port=8390, reload=False)
|
||||||
9
jirorian/nostr.py
Normal file
9
jirorian/nostr.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Nostr relay subscription for kind:5392 registration events.
|
||||||
|
# Runs as a background task alongside the FastAPI app.
|
||||||
|
# Auto-whitelists sender pubkeys and optionally sends kind:4 DM confirmations.
|
||||||
|
|
||||||
|
|
||||||
|
async def listen_for_registrations():
|
||||||
|
# TODO: connect to relay via websockets, subscribe to kind:5392 events
|
||||||
|
# addressed to SERVICE_NOSTR_PUBKEY, call store.whitelist_pubkey() on receipt.
|
||||||
|
raise NotImplementedError
|
||||||
26
jirorian/r2.py
Normal file
26
jirorian/r2.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Cloudflare R2 presigned URL generation via boto3 (S3-compatible API).
|
||||||
|
# Credentials are loaded from environment variables at startup.
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
from botocore.config import Config
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
global _client
|
||||||
|
if _client is None:
|
||||||
|
import os
|
||||||
|
_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=os.environ["R2_ENDPOINT"],
|
||||||
|
aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
|
||||||
|
aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
|
||||||
|
config=Config(signature_version="s3v4"),
|
||||||
|
)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
|
||||||
|
def generate_presigned_upload(app: str, pubkey: str, ext: str, content_type: str, ttl: int = 300) -> tuple[str, str]:
|
||||||
|
# TODO: generate a presigned PUT URL and return (upload_url, cdn_url)
|
||||||
|
raise NotImplementedError
|
||||||
33
jirorian/store.py
Normal file
33
jirorian/store.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# SQLite-backed whitelist and per-pubkey rate limiting.
|
||||||
|
# DB path is configurable via JIRORIAN_DB env var (default: jirorian.db).
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = os.environ.get("JIRORIAN_DB", "jirorian.db")
|
||||||
|
|
||||||
|
|
||||||
|
def init_db():
|
||||||
|
# TODO: create tables: pubkeys(pubkey, app, registered_at, status),
|
||||||
|
# uploads(pubkey, uploaded_at)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def is_whitelisted(pubkey: str) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def is_banned(pubkey: str) -> bool:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def whitelist_pubkey(pubkey: str, app: str):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def count_uploads_today(pubkey: str) -> int:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
def record_upload(pubkey: str):
|
||||||
|
raise NotImplementedError
|
||||||
18
pyproject.toml
Normal file
18
pyproject.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[project]
|
||||||
|
name = "jirorian"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Nostr-native R2 image upload API"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi[standard]>=0.115.0",
|
||||||
|
"boto3>=1.35.0",
|
||||||
|
"pynostr>=0.6.2",
|
||||||
|
"websockets>=13.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
jirorian = "jirorian.main:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.11.0,<0.12.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue