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