diff --git a/README.md b/README.md
index 73156d0..c52f06e 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,6 @@ Code generated by LLMs. Built by one person.
## Features
- **Personal search index** — Save pages you find valuable, search them with full-text search (SQLite FTS5)
-- **RNS live browsing** — Browse any RNS site through your instance via `` tag injection, with LRU caching
-- **Unified add form** — Add HTTP URLs or RNS destination hashes through a single form; auto-detection handles both
- **Tagging** — Organize saved pages with comma-separated tags
- **Bookmarklet** — One-click indexing from any browser tab
- **Subscriptions** — Subscribe to friends' TinyWeb instances over Reticulum and search their indexes alongside yours
@@ -119,14 +117,14 @@ Data persists in the `tinyweb-data` named volume. On Linux with LAN auto-discove
## Storage Estimates
-Pages are stored as cleaned text (HTML tags stripped, boilerplate removed) — typically 5-15 KB per page across both HTTP and RNS sources:
+Average web page content is ~15KB per page:
| Pages | Database | Embeddings* | Total |
|-------|----------|------------|-------|
-| 10,000 | ~100MB | 80MB | ~180MB |
-| 100,000 | ~1GB | 800MB | ~1.8GB |
-| 500,000 | ~5GB | 4GB | ~9GB |
-| 1,000,000 | ~10GB | 8GB | ~18GB |
+| 10,000 | 150MB | 80MB | ~250MB |
+| 100,000 | 1.5GB | 800MB | ~2.5GB |
+| 500,000 | 7.5GB | 4GB | ~12GB |
+| 1,000,000 | 15GB | 8GB | ~25GB |
*Embeddings require semantic search to be enabled. With compression enabled (Settings > Search > AI), embeddings use ~50% less storage.
diff --git a/site_server.py b/site_server.py
deleted file mode 100644
index a1064eb..0000000
--- a/site_server.py
+++ /dev/null
@@ -1,104 +0,0 @@
-import os
-import sys
-import time
-import mimetypes
-
-SITE_DIR = os.path.expanduser("~/apps/tinyweb-site")
-DATA_DIR = os.environ.get("TINYWEB_DATA_DIR") or os.path.expanduser("~/.tinyweb")
-IDENTITY_FILE = "tinyweb-site_identity"
-
-APP_NAME = "tinyweb"
-ASPECTS = ["server"]
-RNS_REQUEST_PATH = "/tinyweb"
-
-import RNS
-
-
-def load_or_create_identity():
- identity_path = os.path.join(DATA_DIR, IDENTITY_FILE)
- if os.path.isfile(identity_path):
- return RNS.Identity.from_file(identity_path)
- identity = RNS.Identity()
- os.makedirs(DATA_DIR, exist_ok=True)
- identity.to_file(identity_path)
- os.chmod(identity_path, 0o600)
- return identity
-
-
-def main():
- configdir = os.environ.get("RNS_CONFIG_DIR")
- reticulum = RNS.Reticulum(configdir=configdir)
-
- identity = load_or_create_identity()
-
- destination = RNS.Destination(
- identity,
- RNS.Destination.IN,
- RNS.Destination.SINGLE,
- APP_NAME,
- *ASPECTS,
- )
-
- destination.register_request_handler(
- RNS_REQUEST_PATH,
- response_generator=request_handler,
- allow=RNS.Destination.ALLOW_ALL,
- )
-
- destination.announce()
- dest_hash = destination.hash.hex()
- print(f"tinyweb-site server running!")
- print(f"Destination hash: <{dest_hash}>")
- print(f"Add this hash to a TinyWeb instance as a mesh site to browse.")
-
- try:
- while True:
- time.sleep(1)
- except KeyboardInterrupt:
- print("\nShutting down...")
- destination.unregister_request_handler()
-
-
-def request_handler(path, data, request_id, link_id, remote_identity, requested_at):
- if data is None:
- data = {"method": "GET", "path": "/", "query": {}, "body": {}, "gateway_host": ""}
- req_path = data.get("path", "/")
-
- if req_path in ("/", "/index.html") or not req_path.strip("/"):
- fs_path = os.path.join(SITE_DIR, "index.html")
- else:
- fs_path = os.path.join(SITE_DIR, req_path.lstrip("/"))
-
- real_path = os.path.realpath(fs_path)
- site_real = os.path.realpath(SITE_DIR)
-
- if not real_path.startswith(site_real + os.sep) and real_path != site_real:
- body = f"
404 Not Found
{req_path}
"
- return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}}
-
- if not os.path.isfile(real_path):
- real_path = os.path.join(SITE_DIR, "index.html")
-
- if not os.path.isfile(real_path):
- body = f"404 Not Found
"
- return {"status": 404, "content_type": "text/html; charset=utf-8", "body": body, "headers": {}}
-
- with open(real_path, "rb") as f:
- content = f.read()
-
- content_type, _ = mimetypes.guess_type(real_path)
- if not content_type:
- content_type = "text/html; charset=utf-8"
- elif content_type.startswith("text/"):
- content_type += "; charset=utf-8"
-
- return {
- "status": 200,
- "content_type": content_type,
- "body": content.decode("utf-8", errors="replace"),
- "headers": {},
- }
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/tinyweb/app.py b/src/tinyweb/app.py
index 10b756f..b63ed3c 100644
--- a/src/tinyweb/app.py
+++ b/src/tinyweb/app.py
@@ -73,22 +73,11 @@ def load_or_create_identity():
return identity
-# Remote peers on the Reticulum mesh can reach read-only public pages.
-# Only GET is allowed; POST is blocked because CSRF cannot authenticate
-# mesh callers (the attacker controls both the "cookie" and the "form" side).
-_RNS_ALLOWED_GET = {
- "/", "/about", "/api/sites", "/share/preview",
-}
-
-_RNS_ALLOWED_PREFIXES = ("/pages", "/tags", "/api/sites", "/rns")
-
-
-def _rns_is_allowed(method, path):
- if method != "GET":
- return False
- if path in _RNS_ALLOWED_GET:
- return True
- return any(path.startswith(p) for p in _RNS_ALLOWED_PREFIXES)
+# Remote peers on the Reticulum mesh can only reach a narrow, read-only surface.
+# Any other method/path is rejected here — CSRF cannot authenticate mesh callers
+# (the attacker controls both the "cookie" and the "form" side of the check), so
+# gating by whitelist is the only safe option.
+_RNS_ALLOWED = {("GET", "/api/sites")}
def rns_request_handler(path, data, request_id, link_id, remote_identity, requested_at):
@@ -96,7 +85,7 @@ def rns_request_handler(path, data, request_id, link_id, remote_identity, reques
data = {"method": "GET", "path": "/", "query": {}, "body": {}, "gateway_host": ""}
method = data.get("method", "GET")
req_path = data.get("path", "/")
- if not _rns_is_allowed(method, req_path):
+ if (method, req_path) not in _RNS_ALLOWED:
return {
"status": 403,
"content_type": "text/plain; charset=utf-8",
diff --git a/src/tinyweb/db.py b/src/tinyweb/db.py
index c8138dc..0e5cea2 100644
--- a/src/tinyweb/db.py
+++ b/src/tinyweb/db.py
@@ -241,13 +241,6 @@ def init_db():
VALUES (new.id, new.title, new.url, new.note);
END;
""")
- db.execute(
- "CREATE TABLE IF NOT EXISTS mesh_sites ("
- " hash TEXT PRIMARY KEY,"
- " name TEXT DEFAULT '',"
- " added_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%S','now'))"
- ")"
- )
# Migrate old subscriptions table if needed
cols = [row[1] for row in db.execute("PRAGMA table_info(subscriptions)").fetchall()]
if "url" in cols and "dest_hash" not in cols:
diff --git a/src/tinyweb/handlers/__init__.py b/src/tinyweb/handlers/__init__.py
index 14a37fd..524ae8d 100644
--- a/src/tinyweb/handlers/__init__.py
+++ b/src/tinyweb/handlers/__init__.py
@@ -6,7 +6,7 @@ from urllib.parse import unquote
from tinyweb.db import get_db, return_db, set_setting
import tinyweb.templates as templates_mod
from tinyweb.templates import esc, wrap_page
-from tinyweb.rns_client import fetch_remote_sites, fetch_remote_page
+from tinyweb.rns_client import fetch_remote_sites
from ._helpers import (
_request_local, _get_csrf_token, _csrf_field, _check_csrf,
@@ -38,9 +38,6 @@ from .data import (
handle_export, handle_import_form, handle_import_submit,
handle_reindex_form, handle_reindex_submit, _reindex_thread,
)
-from .rns import (
- handle_rns_delete_hash, handle_rns_browse,
-)
forum_plugin = None
@@ -90,13 +87,6 @@ def _dispatch_inner(data):
elif path.startswith("/tags/"):
tag_name = unquote(path[len("/tags/"):])
return handle_tag_browse(tag_name, query) if tag_name else _error(400)
- elif path.startswith("/rns/"):
- # /rns//
- parts = path[len("/rns/"):].split("/", 1)
- dest_hash = parts[0]
- if not dest_hash:
- return _error(404)
- return handle_rns_browse(path, dest_hash)
elif path == "/reindex":
return handle_reindex_form()
elif path == "/api/sites":
@@ -165,8 +155,6 @@ def _dispatch_inner(data):
return handle_subscription_delete(sid) if sid is not None else _error(400)
elif path == "/subscriptions/syncall":
return handle_subscription_syncall()
- elif path == "/rns/delete":
- return handle_rns_delete_hash(body)
return _error(404)
diff --git a/src/tinyweb/handlers/pages.py b/src/tinyweb/handlers/pages.py
index 05d885a..4eb2a12 100644
--- a/src/tinyweb/handlers/pages.py
+++ b/src/tinyweb/handlers/pages.py
@@ -29,11 +29,11 @@ def handle_add_form(msg="", action_type="index", prefill_url=""):
)
url_value = f'value="{esc(prefill_url)}" ' if prefill_url else ""
return _respond(
- f"add site
"
- f"Add a site to your index — URL or RNS destination hash
"
+ f"add url
"
+ f"Add a site to your index
"
f'