feat: integrate Just Works app and native business setup

Preserve hosted website UX inside Archipelago, add isolated Business and owner-authorized native wallet setup, and include deployment handoff. Based on main with native signer import already accepted.
This commit is contained in:
yaya
2026-10-08 19:39:38 +01:00
parent 8d17597202
commit 94899e69d3
152 changed files with 22873 additions and 2 deletions
+906
View File
@@ -0,0 +1,906 @@
#!/usr/bin/env python3
import json
import re
import ipaddress
import mimetypes
import os
import secrets
import socket
import sqlite3
import subprocess
import threading
import business_security as security
import business_rollout as rollout
import screen_activity
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from http.cookies import CookieError
from pathlib import Path
from urllib.parse import parse_qs, urlparse
from urllib.parse import urlencode
from urllib.error import HTTPError
from urllib.request import Request, urlopen, build_opener, HTTPRedirectHandler
from urllib.parse import quote
ROOT = Path(__file__).resolve().parent
PUBLIC = ROOT / "public"
NAPLET_DIST = ROOT / "napplets/order-to-table/dist"
DATA_DIR = Path(os.environ.get("JWB_DATA_DIR", ROOT / "data"))
DB_FILE = DATA_DIR / "business.sqlite3"
HOST = os.environ.get("JWB_HOST", "127.0.0.1")
PORT = int(os.environ.get("JWB_PORT", "4310"))
BASE = os.environ.get("JWB_BASE_PATH", "/business-demo").rstrip("/")
LOCK = threading.RLock()
PAYMENT_HELPER = ROOT / "payment-helper.cjs"
PAYMENT_RELAYS = ["wss://relay.primal.net", "wss://nos.lol"]
PAYMENT_RECIPIENT = "f81611363554b64306467234d7396ec88455707633f54738f6c4683535098cd3"
LNURL_URL = "https://primal.net/.well-known/lnurlp/fucking"
DEV_LIGHTNING_ADDRESS = "fucking@primal.net" # dev stand-in matching LNURL_URL, used when a merchant has no real lud16
LNURL_CALLBACK = "https://primal.net/lnurlp/fucking/callback"
MODULE_DEFAULTS = {
"order": (1, "live"), "pay": (1, "live"), "payandgo": (1, "preview"),
"booking": (1, "live"), "screen": (1, "preview"),
"gallery": (1, "preview"), "stats": (1, "live"), "events": (0, "coming-soon"),
}
def now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def db():
DATA_DIR.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_FILE, timeout=10)
conn.row_factory = sqlite3.Row
security.migrate(conn)
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE IF NOT EXISTS merchant (
id TEXT PRIMARY KEY, name TEXT NOT NULL, module_enabled INTEGER NOT NULL,
table_count INTEGER NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, name TEXT NOT NULL,
price_cents INTEGER NOT NULL, enabled INTEGER NOT NULL, sort_order INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tables (
merchant_id TEXT NOT NULL, table_number INTEGER NOT NULL, token TEXT NOT NULL UNIQUE,
PRIMARY KEY(merchant_id, table_number)
);
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, table_number INTEGER NOT NULL,
items_json TEXT NOT NULL, note TEXT NOT NULL, status TEXT NOT NULL,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS payments (
id TEXT PRIMARY KEY, order_id TEXT NOT NULL, merchant_id TEXT NOT NULL,
table_number INTEGER NOT NULL, amount_cents INTEGER NOT NULL,
amount_sats INTEGER NOT NULL, bolt11 TEXT NOT NULL, zap_pubkey TEXT NOT NULL,
status TEXT NOT NULL, receipt_id TEXT, created_at TEXT NOT NULL, settled_at TEXT
);
CREATE TABLE IF NOT EXISTS quick_payments (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, merchant_name TEXT NOT NULL,
amount_cents INTEGER NOT NULL, amount_sats INTEGER NOT NULL, bolt11 TEXT NOT NULL,
zap_pubkey TEXT, recipient TEXT, status TEXT NOT NULL, receipt_id TEXT,
created_at TEXT NOT NULL, settled_at TEXT
);
CREATE TABLE IF NOT EXISTS bookings (
id TEXT PRIMARY KEY, merchant_id TEXT NOT NULL, guest_name TEXT NOT NULL,
guests INTEGER NOT NULL, scheduled_at TEXT NOT NULL, mode TEXT NOT NULL,
notes TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS merchant_modules (
merchant_id TEXT NOT NULL, module_id TEXT NOT NULL, enabled INTEGER NOT NULL,
status TEXT NOT NULL, config_json TEXT NOT NULL, updated_at TEXT NOT NULL,
PRIMARY KEY(merchant_id,module_id)
);
""")
legacy = rollout.legacy_merchant(conn)
conn.execute("INSERT OR IGNORE INTO merchant VALUES (?,'LocalPub Madeira — Napplet Demo',1,30,?)", (legacy, now()))
defaults = [("poncha", "Poncha Fisherman", 350, 1), ("nikita", "Nikita", 450, 2), ("coral", "Coral", 250, 3)]
for pid, name, cents, order in defaults:
conn.execute("INSERT OR IGNORE INTO products VALUES (?, ?, ?, ?, 1, ?)", (pid, legacy, name, cents, order))
for table in range(1, 31):
conn.execute("INSERT OR IGNORE INTO tables VALUES (?, ?, ?)", (legacy, table, secrets.token_urlsafe(18)))
for module_id, (enabled, status) in MODULE_DEFAULTS.items():
conn.execute("INSERT OR IGNORE INTO merchant_modules VALUES (?,?,?,?,?,?)",
(legacy, module_id, enabled, status, '{}', now()))
conn.execute("UPDATE merchant_modules SET status='live' WHERE module_id='booking' AND status='preview'")
conn.commit()
return conn
def helper(payload):
result = subprocess.run(["node", str(PAYMENT_HELPER)], input=json.dumps(payload), text=True, capture_output=True, timeout=12, check=True)
return json.loads(result.stdout)
def fetch_json(url):
request = Request(url, headers={"User-Agent": "JustWorks-Business/0.1"})
with urlopen(request, timeout=10) as response:
return json.loads(response.read())
def screen_tips(npub, lightning_address):
"""Read-only LNURL metadata + signed NIP-57 announcements; no invoice creation."""
if lightning_address.count('@') != 1:
raise ValueError('invalid Lightning address')
name, host = lightning_address.split('@')
if not name or not host or any(char in host for char in '/?#:@'):
raise ValueError('invalid Lightning address')
endpoint = assert_public_https(f'https://{host}/.well-known/lnurlp/{quote(name, safe="")}')
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
raise ValueError('LNURL redirect is not permitted for screen metadata')
with build_opener(NoRedirect).open(Request(endpoint, headers={'User-Agent': 'JustWorks-Screen/1'}), timeout=4) as response:
raw = response.read(65537)
if len(raw) > 65536:
raise ValueError('LNURL metadata too large')
pay = json.loads(raw)
provider = pay.get('nostrPubkey')
if pay.get('tag') != 'payRequest' or pay.get('allowsNostr') is not True or not isinstance(provider, str) or len(provider) != 64 or any(c not in '0123456789abcdef' for c in provider):
raise ValueError('No verified Nostr receipt source')
return helper({'action': 'screen-tips', 'npub': npub, 'provider': provider, 'relays': PAYMENT_RELAYS})['tips']
def assert_public_https(url):
parsed = urlparse(url)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
raise ValueError("Lightning provider returned an unsafe URL")
addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443, type=socket.SOCK_STREAM)
if not addresses or any(not ipaddress.ip_address(item[4][0]).is_global for item in addresses):
raise ValueError("Lightning provider must use a public host")
return url
def lightning_invoice(lightning_address, amount_msat, comment=""):
if "@" not in lightning_address:
raise ValueError("Merchant Lightning address is invalid")
name, host = lightning_address.rsplit("@", 1)
if not name or not host or any(char in host for char in "/?#"):
raise ValueError("Merchant Lightning address is invalid")
endpoint = assert_public_https(f"https://{host}/.well-known/lnurlp/{name}")
pay = fetch_json(endpoint)
if pay.get("tag") != "payRequest" or not pay.get("callback"):
raise ValueError("Lightning address does not support payments")
minimum, maximum = int(pay.get("minSendable", 0)), int(pay.get("maxSendable", 0))
if amount_msat < minimum or (maximum and amount_msat > maximum):
raise ValueError(f"Amount must be between {max(1, minimum // 1000)} and {maximum // 1000} sats")
callback = assert_public_https(str(pay["callback"]))
# Some LNURL providers reject otherwise valid NIP-57 requests when their
# signed content contains a literal percent sign. Keep the human meaning
# while using a provider-safe comment for both the zap and callback.
safe_comment = str(comment or "").replace("%", " percent").strip()
params = {"amount": amount_msat}
proof = None
recipient = str(pay.get("nostrPubkey") or "").strip()
if pay.get("allowsNostr") is True and len(recipient) == 64:
proof = helper({"action": "create", "lnurlUrl": endpoint, "relays": PAYMENT_RELAYS,
"amountMsat": amount_msat, "recipient": recipient,
"content": safe_comment or "Just Works payment"})
params["nostr"] = json.dumps(proof["event"], separators=(",", ":"))
params["lnurl"] = proof["lnurl"]
if safe_comment and int(pay.get("commentAllowed", 0)):
params["comment"] = safe_comment[:int(pay["commentAllowed"])]
separator = "&" if "?" in callback else "?"
try:
invoice = fetch_json(f"{callback}{separator}{urlencode(params)}")
except HTTPError:
if not proof:
raise
# Invoice creation is the primary action. If a provider advertises
# NIP-57 but fails its zap path, retry as standard LNURL-pay and mark
# the result non-verifiable instead of blocking the customer.
proof = None
fallback = {key: value for key, value in params.items() if key not in {"nostr", "lnurl"}}
invoice = fetch_json(f"{callback}{separator}{urlencode(fallback)}")
if invoice.get("status") == "ERROR" or not invoice.get("pr"):
raise ValueError(invoice.get("reason", "Lightning invoice unavailable"))
return {"bolt11": invoice["pr"], "zap_pubkey": proof["pubkey"] if proof else None,
"recipient": recipient if proof else None, "verifiable": bool(proof)}
def remote_json(url, method="GET", payload=None, timeout=60):
data = json.dumps(payload).encode() if payload is not None else None
request = Request(url, data=data, method=method, headers={"User-Agent": "JustWorks-Business/0.1", "Content-Type": "application/json"})
try:
with urlopen(request, timeout=timeout) as response:
return json.loads(response.read())
except HTTPError as error:
try: message = json.loads(error.read()).get("error")
except Exception: message = None
raise ValueError(message or f"Just Works profile request failed ({error.code})") from error
def resolve_merchant_npub(npub):
npub = security.canonical_npub(npub)
# Core resolves every published website; the regional directory is optional
# presentation metadata, never a merchant allowlist or ownership proof.
try:
nsite = fetch_json(f"https://justworks.cash/api/nsites/{npub}").get("nsite", {})
except HTTPError as error:
if error.code == 404: return None, None
raise
slug = nsite.get("id", "")
if (nsite.get("npub") != npub or not isinstance(slug, str)
or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", slug)
or nsite.get("nsite", {}).get("deploymentStatus") != "published"):
return None, None
place = {"npub": npub, "slug": slug, "name": nsite.get("name") or slug,
"justworks": f"https://justworks.cash/{slug}"}
return place, nsite
def merchant_payload(conn, include_tokens=False):
merchant = dict(conn.execute("SELECT * FROM merchant WHERE id=?", (rollout.legacy_merchant(conn),)).fetchone())
merchant["module_enabled"] = bool(merchant["module_enabled"])
merchant["products"] = [dict(row) for row in conn.execute("SELECT id,name,price_cents,enabled,sort_order FROM products WHERE merchant_id=? ORDER BY sort_order", (merchant["id"],))]
for product in merchant["products"]:
product["enabled"] = bool(product["enabled"])
merchant["modules"] = {}
for row in conn.execute("SELECT module_id,enabled,status,config_json FROM merchant_modules WHERE merchant_id=?", (merchant["id"],)):
merchant["modules"][row["module_id"]] = {"enabled": bool(row["enabled"]), "status": row["status"], "config": json.loads(row["config_json"] or '{}')}
if include_tokens:
merchant["tables"] = [dict(row) for row in conn.execute("SELECT table_number,token FROM tables WHERE merchant_id=? ORDER BY table_number", (merchant["id"],))]
return merchant
def merchant_modules(conn, merchant_id):
for module_id, (enabled, status) in MODULE_DEFAULTS.items():
conn.execute("INSERT OR IGNORE INTO merchant_modules VALUES (?,?,?,?,?,?)",
(merchant_id, module_id, enabled, status, '{}', now()))
conn.commit()
return {row["module_id"]: {"enabled": bool(row["enabled"]), "status": row["status"], "config": json.loads(row["config_json"] or '{}')}
for row in conn.execute("SELECT module_id,enabled,status,config_json FROM merchant_modules WHERE merchant_id=?", (merchant_id,))}
class Handler(BaseHTTPRequestHandler):
server_version = "JustWorksBusiness/0.1"
def log_message(self, fmt, *args):
# Query strings may contain table tokens. Do not log request lines or bodies.
print(json.dumps({"ts": now(), "method": self.command, "status": str(args[1]) if len(args) > 1 else ""}))
def send_json(self, status, value):
mutation = getattr(self, "authorized_mutation", None)
if mutation and 200 <= status < 300:
with LOCK, db() as conn:
security.audit(conn, "mutation", self.route()[0], *mutation)
self.authorized_mutation = None
raw = json.dumps(value, separators=(",", ":")).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.send_header("Cache-Control", "no-store")
if getattr(self, "session_cookie", None):
self.send_header("Set-Cookie", self.session_cookie)
if getattr(self, "exchange_cookie", None):
self.send_header("Set-Cookie", self.exchange_cookie)
self.send_header("Referrer-Policy", "no-referrer")
self.end_headers()
self.wfile.write(raw)
def redirect(self, location):
self.send_response(302)
self.send_header("Location", location)
self.send_header("Cache-Control", "public,max-age=3600")
self.end_headers()
def body(self):
size = int(self.headers.get("Content-Length", "0"))
if size < 0 or size > 64 * 1024:
raise ValueError("body too large")
value = json.loads(self.rfile.read(size) or b"{}")
if not isinstance(value, dict): raise ValueError("JSON object required")
return value
def route(self):
parsed = urlparse(self.path)
path = parsed.path
if BASE and path.startswith(BASE):
path = path[len(BASE):] or "/"
return path, parse_qs(parsed.query)
def surface(self):
host = self.headers.get("Host", "").split(":", 1)[0].lower()
return host.removesuffix(".justworks.cash") if host.endswith(".justworks.cash") else ""
def is_local(self):
if os.environ.get("JWB_ARCHIPELAGO") == "1": return False
# Anything not served behind the production subdomains is a dev server.
host = self.headers.get("Host", "").split(":", 1)[0].lower()
return not host.endswith(".justworks.cash")
def static(self, file, cache="no-store"):
file = file.resolve()
allowed = [PUBLIC.resolve(), NAPLET_DIST.resolve()]
if not any(file == root or root in file.parents for root in allowed) or not file.is_file():
self.send_error(404)
return
raw = file.read_bytes()
if os.environ.get("JWB_ARCHIPELAGO") == "1" and file.suffix == ".html":
html = raw.decode().replace('"/assets/', '"/business-demo/assets/').replace('"/fonts/', '"/business-demo/fonts/')
html = html.replace('<head>', '<head><script src="/business-demo/assets/archipelago-paths.js"></script>', 1)
raw = html.encode()
mime = mimetypes.guess_type(file.name)[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", mime)
self.send_header("Content-Length", str(len(raw)))
self.send_header("Cache-Control", cache)
self.send_header("X-Content-Type-Options", "nosniff")
if file.name == "business-sso.html":
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'")
self.end_headers()
self.wfile.write(raw)
def nsite(self):
file = PUBLIC / "nsite.html"
html = file.read_text()
html = html.replace("</body>", f'<script type="module" src="{BASE}/assets/nsite-live.js"></script></body>')
raw = html.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(raw)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(raw)
def app_manifest(self, app, query):
manifest = json.loads((PUBLIC / f"{app}.webmanifest").read_text())
npub = (query.get("npub") or [""])[0]
if npub:
try: npub = security.canonical_npub(npub)
except ValueError: return self.send_json(400, {"error": "invalid npub"})
manifest["start_url"] = f"/?npub={npub}"
manifest["id"] += f"&npub={npub}"
raw = json.dumps(manifest, separators=(",", ":")).encode()
self.send_response(200)
self.send_header("Content-Type", "application/manifest+json")
self.send_header("Content-Length", str(len(raw)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.end_headers()
self.wfile.write(raw)
def do_GET(self):
self.session_cookie = self.exchange_cookie = self.authorized_mutation = None
path, query = self.route()
if path == "/api/business-session" or path in security.PRIVILEGED_GET:
with LOCK, db() as conn:
if path == "/api/business-session":
current = security.session(conn, self.headers)
return self.send_json(200, {"authenticated": bool(current), "session": current, "mode": security.mode(), "sso_enabled": rollout.enabled() and ('https://' + self.headers.get('Host', '').lower()) in rollout.origins(), "legacy_npub": rollout.legacy_merchant(conn) if rollout.legacy_merchant(conn) != 'localpub-demo' else None})
allowed, _ = security.authorize(conn, "GET", path, {key: value[0] for key, value in query.items()}, self.headers)
if not allowed: return self.send_json(403, {"error": "Merchant session required for this account"})
if path == "/healthz": return self.send_json(200, {"ok": True})
if path == "/api/config":
with LOCK, db() as conn: return self.send_json(200, merchant_payload(conn))
if path == "/api/dashboard":
with LOCK, db() as conn:
payload = merchant_payload(conn, True)
payload["orders"] = [dict(row) | {"items": json.loads(row["items_json"])} for row in conn.execute("SELECT * FROM orders WHERE merchant_id=? AND status NOT IN ('delivered','closed') ORDER BY created_at DESC LIMIT 50", (rollout.legacy_merchant(conn),))]
for item in payload["orders"]: item.pop("items_json", None)
payload["payments"] = [dict(row) for row in conn.execute("SELECT id,order_id,table_number,amount_cents,amount_sats,status,created_at,settled_at FROM payments WHERE merchant_id=? AND status='paid' ORDER BY settled_at DESC LIMIT 20", (rollout.legacy_merchant(conn),))]
return self.send_json(200, payload)
if path == "/api/business":
npub = (query.get("npub") or [""])[0].strip()
try:
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
with LOCK, db() as conn:
modules = merchant_modules(conn, npub)
counts = {
"orders": conn.execute("SELECT COUNT(*) FROM orders WHERE merchant_id=? AND status!='closed'", (npub,)).fetchone()[0],
"payments": conn.execute("SELECT COUNT(*) FROM payments WHERE merchant_id=? AND status NOT IN ('acknowledged')", (npub,)).fetchone()[0],
"bookings": conn.execute("SELECT COUNT(*) FROM bookings WHERE merchant_id=? AND status NOT IN ('seated','closed','cancelled')", (npub,)).fetchone()[0],
}
current = security.session(conn, self.headers)
if rollout.effective_mode(conn, "GET", "/api/business", npub) == "enforce" and (not current or current["npub"] != npub):
counts = {key: 0 for key in counts}
images = list(dict.fromkeys([nsite.get("media", {}).get("heroImage"), *nsite.get("media", {}).get("galleryImages", []), *place.get("images", [])]))
images = [image for image in images if image and not any(marker in image.lower() for marker in ("logo", "icon", "static.cdninstagram.com"))]
merchant_name = nsite.get("name") or place.get("name")
query_string = urlencode({"q": merchant_name})
map_query_string = urlencode({"q": merchant_name, "place": place.get("slug")})
links = {
"justworks": place.get("justworks") or f"https://justworks.cash/{place.get('slug')}",
"tip": f"https://justworks.cash/tip/?{urlencode({'id': place.get('slug')})}",
"nsite": nsite.get("nsite", {}).get("gatewayUrl"),
"nostr": nsite.get("identity", {}).get("primalUrl") or f"https://njump.me/{npub}",
"maps": f"https://maps.justworks.cash/?{map_query_string}",
"gallery": f"https://gallery.justworks.cash/?{query_string}",
}
if self.is_local():
# Dev: keep merchants on local surfaces — /page renders the
# platform-hosted Just Works page, /site renders the nsite
# copy the product generates and publishes to Nostr.
links["justworks"] = f"{BASE}/page?npub={npub}"
links["nsite"] = f"{BASE}/site?npub={npub}"
links["gallery"] = f"{BASE}/gallery?{query_string}"
profile = nsite.get("identity", {}).get("profile", {})
lightning_address = str(profile.get("lud16") or "").strip()
payment_ready = bool(lightning_address and not lightning_address.lower().endswith("@npub.cash"))
if self.is_local() and not payment_ready:
# Dev only: stand in a working Lightning address so Pay / Pay & Go are unlocked.
lightning_address = DEV_LIGHTNING_ADDRESS
payment_ready = True
return self.send_json(200, {"merchant": {"name": merchant_name, "npub": npub, "slug": place.get("slug"), "area": place.get("area"), "category": place.get("category")}, "images": images[:8], "links": links, "profile": profile, "lightning_address": lightning_address, "payment_ready": payment_ready, "modules": modules, "counts": counts})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"merchant unavailable: {error}"})
if path == "/api/nsite-page":
npub = (query.get("npub") or [""])[0].strip()
try:
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
sections = nsite.get("page", {}).get("sections", [])
menu = next((section for section in sections if section.get("type") == "menu" and section.get("enabled")), {})
products = [{"id": str(item.get("id") or f"item-{index}")[:40], "name": str(item.get("title") or item.get("name") or "Item")[:80], "description": str(item.get("body") or item.get("description") or "")[:180], "price": str(item.get("price") or "")[:30]} for index, item in enumerate(menu.get("items", [])[:60])]
assets = [item.get("url") for item in nsite.get("page", {}).get("assets", []) if item.get("url")]
sources = [image for source in nsite.get("sources", []) for image in source.get("images", [])]
media = nsite.get("media", {})
images = [image for image in dict.fromkeys([media.get("heroImage"), *media.get("galleryImages", []), *assets, *sources, *place.get("images", [])]) if image and not any(marker in image.lower() for marker in ("logo", "icon", "static.cdninstagram.com"))][:12]
profile = nsite.get("identity", {}).get("profile", {})
sources = [{"name": str(source.get("title") or source.get("host") or "Source")[:80], "host": str(source.get("host") or "")[:60], "url": str(source.get("url") or "")[:300], "description": str(source.get("description") or "")[:180], "photos": len(source.get("images", []))} for source in nsite.get("sources", [])][:6]
return self.send_json(200, {
"merchant": {"name": nsite.get("name") or place.get("name"), "npub": npub, "slug": place.get("slug"), "area": place.get("area"), "category": place.get("category"), "address": place.get("address"), "hours": place.get("hours"), "phone": place.get("phone"), "website": place.get("website"), "tags": [tag for tag in place.get("tags", [])[:8] if tag], "rating": place.get("rating")},
"about": str(profile.get("about") or "").strip()[:600],
"products": products,
"images": images,
"sources": sources,
"gateway": nsite.get("nsite", {}).get("gatewayUrl", ""),
"links": {"business": f"{BASE}/dashboard?npub={npub}", "page": f"{BASE}/page?npub={npub}"},
})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"site unavailable: {error}"})
if path == "/api/qr":
text = (query.get("text") or [""])[0].strip()
if not text or len(text) > 500: return self.send_json(400, {"error": "provide qr text"})
try:
raw = helper({"action": "qr", "text": text})["svg"].encode()
self.send_response(200)
self.send_header("Content-Type", "image/svg+xml")
self.send_header("Cache-Control", "public,max-age=3600")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
return
except Exception as error: return self.send_json(502, {"error": f"qr unavailable: {error}"})
if path == "/api/gallery":
try:
payload = fetch_json("https://justworks.cash/data/regions/madeira.json")
return self.send_json(200, {"name": payload.get("name", "Madeira"), "places": payload.get("places", [])})
except Exception as error:
return self.send_json(502, {"error": f"gallery data unavailable: {error}"})
if path == "/api/screen":
npub = (query.get("npub") or [""])[0].strip()
try:
npub = security.canonical_npub(npub)
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
screen_activity.configure(npub, nsite)
assets = [item.get("url") for item in nsite.get("page", {}).get("assets", []) if item.get("url")]
sources = [image for source in nsite.get("sources", []) for image in source.get("images", [])]
images = list(dict.fromkeys([nsite.get("media", {}).get("heroImage"), *nsite.get("media", {}).get("galleryImages", []), *assets, *sources, *place.get("images", [])]))
images = [image for image in images if image and not any(marker in image.lower() for marker in ("static.cdninstagram.com", "facebook.com/images", "logo", "icon"))][:18]
images.sort(key=lambda image: (image.lower().split("?", 1)[0].endswith(".png"), "folha" in image.lower()))
order_url = f"http://{self.headers.get('Host', '127.0.0.1:4310')}{BASE}/order?npub={npub}" if self.is_local() else f"https://order.justworks.cash/?npub={npub}"
justpay_url = f"http://{self.headers.get('Host', '127.0.0.1:4310')}{BASE}/portal?{urlencode({'surface': 'payandgo', 'npub': npub})}" if self.is_local() else f"https://payandgo.justworks.cash/?npub={npub}"
tip_url = f"https://justworks.cash/tip/?{urlencode({'id': place['slug']})}"
qr = helper({"action": "qr", "text": order_url})["svg"]
try: latest = helper({"action": "latest", "npub": npub, "relays": PAYMENT_RELAYS})["event"]
except Exception: latest = None
return self.send_json(200, {"merchant": {"name": nsite.get("name") or place.get("name"), "npub": npub, "slug": place.get("slug"), "area": place.get("area"), "category": place.get("category")}, "images": images, "latest": latest, "order_url": order_url, "qr_svg": qr, "screen_links": {"order": order_url, "justpay": justpay_url, "tip": tip_url}})
except ValueError as error:
return self.send_json(400, {"error": str(error)})
except Exception as error:
return self.send_json(502, {"error": f"screen data unavailable: {error}"})
if path == "/api/screen-activity":
try:
npub = security.canonical_npub((query.get('npub') or [''])[0].strip())
except ValueError:
return self.send_json(400, {'error': 'provide a valid merchant npub'})
with LOCK, db() as conn:
payload = screen_activity.projection(conn, npub, screen_tips)
return self.send_json(200, payload)
if path == "/api/order-merchant":
npub = (query.get("npub") or [""])[0].strip()
try:
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
sections = nsite.get("page", {}).get("sections", [])
menu = next((section for section in sections if section.get("type") == "menu" and section.get("enabled")), {})
products = [{"id": str(item.get("id") or f"item-{index}")[:40], "name": str(item.get("title") or item.get("name") or "Item")[:80], "description": str(item.get("body") or item.get("description") or "")[:180], "price": str(item.get("price") or "")[:30]} for index, item in enumerate(menu.get("items", [])[:50])]
images = list(dict.fromkeys([nsite.get("media", {}).get("heroImage"), *nsite.get("media", {}).get("galleryImages", []), *place.get("images", [])]))
images = [image for image in images if image and not any(marker in image.lower() for marker in ("logo", "icon", "static.cdninstagram.com"))]
images.sort(key=lambda image: image.lower().split("?", 1)[0].endswith(".png"))
return self.send_json(200, {"merchant": {"name": nsite.get("name") or place.get("name"), "npub": npub, "slug": place.get("slug"), "area": place.get("area"), "category": place.get("category")}, "images": images[:8], "products": products})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"merchant unavailable: {error}"})
if path == "/api/merchant-order-status":
npub = (query.get("npub") or [""])[0].strip()
order_id = (query.get("id") or [""])[0].strip()
if not npub.startswith("npub1") or len(npub) > 100 or not order_id:
return self.send_json(400, {"error": "provide the merchant and order"})
with LOCK, db() as conn:
row = conn.execute("SELECT id,table_number,status,updated_at FROM orders WHERE id=? AND merchant_id=?", (order_id, npub)).fetchone()
if not row: return self.send_json(404, {"error": "order not found"})
return self.send_json(200, dict(row))
if path == "/api/quick-pay-status":
npub = (query.get("npub") or [""])[0].strip()
payment_id = (query.get("id") or [""])[0].strip()
if not npub.startswith("npub1") or len(npub) > 100 or len(payment_id) != 32:
return self.send_json(400, {"error": "provide the merchant and payment"})
with LOCK, db() as conn:
row = conn.execute("SELECT * FROM quick_payments WHERE id=? AND merchant_id=?", (payment_id, npub)).fetchone()
if not row: return self.send_json(404, {"error": "payment not found"})
payment = dict(row)
if payment["status"] == "pending" and payment["zap_pubkey"] and payment["recipient"]:
try:
created_at = int(datetime.fromisoformat(payment["created_at"]).timestamp())
check = helper({"action": "check", "relays": PAYMENT_RELAYS, "recipient": payment["recipient"],
"pubkey": payment["zap_pubkey"], "since": created_at})
if check["paid"]:
settled_at = now()
with LOCK, db() as conn:
conn.execute("UPDATE quick_payments SET status='paid',receipt_id=?,settled_at=? WHERE id=?",
(check["receipt"], settled_at, payment_id))
conn.commit()
payment.update(status="paid", receipt_id=check["receipt"], settled_at=settled_at)
except Exception:
pass
fields = ("id", "merchant_name", "amount_cents", "amount_sats", "status", "receipt_id", "created_at", "settled_at")
return self.send_json(200, {key: payment[key] for key in fields} | {"verifiable": bool(payment["zap_pubkey"])})
if path == "/api/pay-orders":
# Existing customer receipt discovery, without staff notes, guest bookings or payments.
npub = (query.get("npub") or [""])[0].strip()
try: security.canonical_npub(npub)
except ValueError: return self.send_json(400, {"error": "provide a valid npub"})
with LOCK, db() as conn:
orders = [{"table_number": row["table_number"], "status": row["status"],
"items": [{key: item.get(key) for key in ("name", "qty", "price_cents")} for item in json.loads(row["items_json"])]}
for row in conn.execute("SELECT table_number,status,items_json FROM orders WHERE merchant_id=? AND status!='closed' ORDER BY created_at", (npub,))]
return self.send_json(200, {"orders": orders})
if path == "/api/pos":
npub = (query.get("npub") or [""])[0].strip()
if not npub.startswith("npub1") or len(npub) > 100:
return self.send_json(400, {"error": "provide a valid npub"})
with LOCK, db() as conn:
orders = [dict(row) | {"items": json.loads(row["items_json"])} for row in conn.execute("SELECT * FROM orders WHERE merchant_id=? AND status!='closed' ORDER BY created_at", (npub,))]
for item in orders: item.pop("items_json", None)
payments = [dict(row) for row in conn.execute("SELECT id,order_id,table_number,amount_cents,amount_sats,status,created_at,settled_at FROM payments WHERE merchant_id=? AND status!='acknowledged' ORDER BY created_at", (npub,))]
bookings = [dict(row) for row in conn.execute("SELECT * FROM bookings WHERE merchant_id=? AND status NOT IN ('closed','cancelled') ORDER BY scheduled_at", (npub,))]
return self.send_json(200, {"orders": orders, "payments": payments, "bookings": bookings, "server_time": now()})
if path == "/api/table":
token = (query.get("token") or [""])[0]
with LOCK, db() as conn:
row = conn.execute("SELECT table_number FROM tables WHERE token=?", (token,)).fetchone()
if not row: return self.send_json(404, {"error": "invalid table token"})
config = merchant_payload(conn)
if not config["module_enabled"]: return self.send_json(403, {"error": "module disabled"})
return self.send_json(200, {"merchant": config["name"], "table": row["table_number"], "products": [p for p in config["products"] if p["enabled"]]})
if path == "/api/order":
token = (query.get("token") or [""])[0]
order_id = (query.get("id") or [""])[0]
with LOCK, db() as conn:
row = conn.execute("""SELECT o.id,o.table_number,o.status,o.updated_at
FROM orders o JOIN tables t ON t.merchant_id=o.merchant_id AND t.table_number=o.table_number
WHERE o.id=? AND t.token=?""", (order_id, token)).fetchone()
if not row: return self.send_json(404, {"error": "order not found"})
return self.send_json(200, dict(row))
if path == "/api/payment":
token = (query.get("token") or [""])[0]
payment_id = (query.get("id") or [""])[0]
with LOCK, db() as conn:
row = conn.execute("""SELECT p.* FROM payments p JOIN tables t ON t.merchant_id=p.merchant_id AND t.table_number=p.table_number
WHERE p.id=? AND t.token=?""", (payment_id, token)).fetchone()
if not row: return self.send_json(404, {"error": "payment not found"})
payment = dict(row)
if payment["status"] == "pending":
check = helper({"action": "check", "relays": PAYMENT_RELAYS, "recipient": PAYMENT_RECIPIENT, "pubkey": payment["zap_pubkey"], "since": int(datetime.fromisoformat(payment["created_at"]).timestamp())})
if check["paid"]:
with LOCK, db() as conn:
conn.execute("UPDATE payments SET status='paid',receipt_id=?,settled_at=? WHERE id=?", (check["receipt"], now(), payment_id))
conn.commit()
payment["status"], payment["receipt_id"], payment["settled_at"] = "paid", check["receipt"], now()
return self.send_json(200, {key: payment[key] for key in ("id", "order_id", "table_number", "amount_cents", "amount_sats", "bolt11", "status", "receipt_id", "created_at", "settled_at")})
if path == "/napplet/order-to-table/index.html": return self.static(NAPLET_DIST / "index.html", "public,max-age=300")
if path == "/assets/examples/localpub-venue.webp": return self.redirect("https://justworks.cash/assets/examples/localpub-venue.webp")
if path == "/nsite": return self.nsite()
if path == "/business-sso": return self.static(PUBLIC / "business-sso.html")
if path in ("/pos-sw.js", "/screen-sw.js"):
return self.static(PUBLIC / path[1:], "no-cache")
if path in ("/pos.webmanifest", "/screen.webmanifest"):
return self.app_manifest(path[1:].split(".")[0], query)
surface_pages = {
"business": "dashboard.html", "pos": "pos.html", "order": "order.html", "book": "book.html",
"pay": "portal.html", "payandgo": "portal.html", "screen": "screen.html",
"gallery": "gallery.html", "events": "portal.html",
}
pages = {"/qr-sheet": "qr-sheet.html", "/": surface_pages.get(self.surface(), "dashboard.html"), "/dashboard": "dashboard.html", "/profile": "profile.html", "/stats": "stats.html", "/embed": "shell.html", "/portal": "portal.html", "/gallery": "gallery.html", "/screen": "screen.html", "/order": "order.html", "/book": "book.html", "/pos": "pos.html", "/site": "site.html", "/page": "page.html"}
if path in pages: return self.static(PUBLIC / pages[path])
if path.startswith("/assets/"): return self.static(PUBLIC / path.removeprefix("/"), "public,max-age=3600")
if path.startswith("/fonts/"): return self.static(PUBLIC / path.removeprefix("/"), "public,max-age=86400")
self.send_error(404)
def do_POST(self):
self.session_cookie = self.exchange_cookie = self.authorized_mutation = None
path, _ = self.route()
try: body = self.body()
except Exception as error: return self.send_json(400, {"error": str(error)})
if path.startswith("/api/business-sso/"):
if not rollout.enabled(): return self.send_json(404, {"error": "Session exchange is not enabled"})
with LOCK, db() as conn:
if not security.rate_allowed(self.client_address[0], "/api/business-owner-login", {}, self.headers):
return self.send_json(429, {"error": "Too many exchange requests"})
try:
if path == "/api/business-sso/start":
result, self.exchange_cookie = rollout.start(conn, body, self.headers)
elif path in {"/api/business-sso/details", "/api/business-sso/approve"}:
result = rollout.source_details(conn, body, self.headers, path.endswith("approve"))
elif path == "/api/business-sso/complete":
result, self.session_cookie = rollout.complete(conn, body, self.headers)
self.exchange_cookie = rollout.exchange_cookie()
else: return self.send_json(404, {"error": "Unknown session action"})
return self.send_json(200, result)
except (ValueError, KeyError, CookieError):
security.audit(conn, "exchange_denied", path, reason="invalid_or_expired_exchange")
return self.send_json(403, {"error": "Session exchange could not be verified; log in and try again"})
self.authorized_mutation = None
self.session_cookie = None
if path in {"/api/business-owner-login", "/api/orders", "/api/merchant-orders", "/api/bookings", "/api/payments", "/api/quick-pay"}:
with LOCK:
if not security.rate_allowed(self.client_address[0], path, body, self.headers):
return self.send_json(429, {"error": "Too many requests; try again shortly"})
if path in {"/api/business-owner-login", "/api/business-logout", "/api/business-revoke"} and not security.same_origin(self.headers):
return self.send_json(403, {"error": "Same-origin request required"})
with LOCK, db() as conn:
if path in {"/api/business-logout", "/api/business-revoke"}:
current = security.revoke(conn, self.headers, path.endswith("revoke"))
security.audit(conn, "logout", path, actor=current["npub"] if current else "")
self.session_cookie = security.cookie_header()
return self.send_json(200, {"ok": True})
allowed, context = security.authorize(conn, "POST", path, body, self.headers)
if not allowed: return self.send_json(403, {"error": "Merchant session required for this account"})
self.authorized_mutation = context
if path == "/api/config":
with LOCK, db() as conn:
enabled = 1 if body.get("module_enabled") else 0
count = max(1, min(100, int(body.get("table_count", 30))))
legacy = rollout.legacy_merchant(conn)
conn.execute("UPDATE merchant SET module_enabled=?,table_count=?,updated_at=? WHERE id=?", (enabled, count, now(), legacy))
products = body.get("products") or []
for index, product in enumerate(products[:50]):
pid = str(product.get("id") or f"item-{index}")[:40]
name = str(product.get("name") or "Item")[:80]
cents = max(0, min(1000000, int(product.get("price_cents", 0))))
conn.execute("INSERT INTO products VALUES (?, ?, ?, ?, 1, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name,price_cents=excluded.price_cents,sort_order=excluded.sort_order WHERE products.merchant_id=excluded.merchant_id", (pid, legacy, name, cents, index))
for table in range(1, count + 1): conn.execute("INSERT OR IGNORE INTO tables VALUES (?, ?, ?)", (legacy, table, secrets.token_urlsafe(18)))
for module_id, value in (body.get("modules") or {}).items():
if module_id not in MODULE_DEFAULTS: continue
enabled = 1 if value.get("enabled") else 0
config = json.dumps(value.get("config") or {}, separators=(",", ":"))
conn.execute("UPDATE merchant_modules SET enabled=?,config_json=?,updated_at=? WHERE merchant_id=? AND module_id=?", (enabled, config, now(), legacy, module_id))
conn.commit()
return self.send_json(200, merchant_payload(conn, True))
if path == "/api/business-config":
npub = str(body.get("npub", "")).strip()
try:
place, _ = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"merchant unavailable: {error}"})
requested = body.get("modules") or {}
with LOCK, db() as conn:
merchant_modules(conn, npub)
for module_id, value in requested.items():
if module_id not in MODULE_DEFAULTS or MODULE_DEFAULTS[module_id][1] == "coming-soon": continue
conn.execute("UPDATE merchant_modules SET enabled=?,updated_at=? WHERE merchant_id=? AND module_id=?", (1 if value else 0, now(), npub, module_id))
conn.commit()
return self.send_json(200, {"ok": True, "modules": merchant_modules(conn, npub)})
if path == "/api/business-owner-login":
npub = str(body.get("npub", "")).strip()
try:
place, _ = resolve_merchant_npub(npub)
if not place: raise ValueError("merchant_not_found")
if os.environ.get("JWB_ALLOW_DEMO_LOGIN") == "1" and security.mode() != "enforce" and str(body.get("ownerIdentifier", "")).strip().lower() == "demo":
# Dev shortcut: prefilled demo login resolves locally without remote credentials.
return self.send_json(200, {"ok": True, "merchant": place.get("name"), "npub": npub, "dev": True})
credentials = {key: str(body.get(key, "")).strip() for key in ("ownerKey", "ownerIdentifier", "ownerPassword") if body.get(key)}
if not credentials.get("ownerKey") and not (credentials.get("ownerIdentifier") and credentials.get("ownerPassword")):
raise ValueError("credentials_required")
result = remote_json(f"https://justworks.cash/api/nsites/{place['slug']}/login", "POST", credentials)
owner = result.get("nsite", {})
if owner.get("npub") != npub:
raise ValueError("ownership_mismatch")
with LOCK, db() as conn:
self.session_cookie, expires = security.issue(conn, npub, self.headers)
rollout.verification(conn, npub, place['slug'])
security.audit(conn, "login_success", path, npub, npub)
return self.send_json(200, {"ok": True, "merchant": owner.get("name") or place.get("name"), "npub": npub, "expires_at": expires})
except Exception:
self.session_cookie = None
with LOCK, db() as conn: security.audit(conn, "login_failure", path, reason="verification_failed")
return self.send_json(401, {"error": "Just Works login could not be verified"})
if path == "/api/business-profile":
npub = str(body.get("npub", "")).strip()
try:
place, _ = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
profile = body.get("profile") or {}
lightning_address = str(profile.get("lud16", "")).strip().lower()
if not lightning_address or "@" not in lightning_address or lightning_address.endswith("@npub.cash"):
return self.send_json(400, {"error": "Enter the merchant’s own Lightning address, not the default npub.cash address"})
credentials = {key: str(body.get(key, "")).strip() for key in ("ownerKey", "ownerIdentifier", "ownerPassword") if body.get(key)}
if not credentials.get("ownerKey") and not (credentials.get("ownerIdentifier") and credentials.get("ownerPassword")):
return self.send_json(400, {"error": "Enter the Just Works owner login or owner key"})
if os.environ.get("JWB_ALLOW_DEMO_LOGIN") == "1" and security.mode() != "enforce" and credentials.get("ownerIdentifier", "").lower() == "demo":
# Dev shortcut: accept the save locally without republishing the production nsite.
dev_profile = {"display_name": str(profile.get("display_name", ""))[:120], "about": str(profile.get("about", ""))[:1200], "picture": str(profile.get("picture", ""))[:1000], "banner": str(profile.get("banner", ""))[:1000], "lud16": lightning_address, "website": place.get("justworks") or ""}
return self.send_json(200, {"ok": True, "profile": dev_profile, "published": True, "dev": True})
update = {
**credentials,
"name": str(profile.get("display_name", ""))[:120],
"note": str(profile.get("about", ""))[:1200],
"payment": lightning_address,
"avatarImage": str(profile.get("picture", ""))[:1000],
"heroImage": str(profile.get("banner", ""))[:1000],
"stage": "Business profile updated",
}
remote_json(f"https://justworks.cash/api/nsites/{place['slug']}", "PATCH", update)
published = remote_json(f"https://justworks.cash/api/nsites/{place['slug']}/deploy", "POST", credentials, 90)
return self.send_json(200, {"ok": True, "profile": published.get("nsite", {}).get("identity", {}).get("profile", {}), "published": True})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"profile unavailable: {error}"})
if path == "/api/orders":
token, items = str(body.get("token", "")), body.get("items") or []
note = str(body.get("note", ""))[:240]
with LOCK, db() as conn:
table = conn.execute("SELECT table_number,merchant_id FROM tables WHERE token=?", (token,)).fetchone()
if not table: return self.send_json(404, {"error": "invalid table token"})
merchant = conn.execute("SELECT module_enabled FROM merchant WHERE id=?", (table['merchant_id'],)).fetchone()
if not merchant["module_enabled"]: return self.send_json(403, {"error": "module disabled"})
clean = []
for item in items[:20]:
product = conn.execute("SELECT id,name,price_cents FROM products WHERE id=? AND merchant_id=? AND enabled=1", (str(item.get("id", "")), table['merchant_id'])).fetchone()
qty = max(0, min(20, int(item.get("qty", 0))))
if product and qty: clean.append(dict(product) | {"qty": qty})
if not clean: return self.send_json(400, {"error": "order is empty"})
oid, timestamp = secrets.token_hex(5), now()
conn.execute("INSERT INTO orders VALUES (?, ?, ?, ?, ?, 'new', ?, ?)", (oid, table['merchant_id'], table["table_number"], json.dumps(clean), note, timestamp, timestamp))
conn.commit()
total_cents = sum(item["price_cents"] * item["qty"] for item in clean)
return self.send_json(201, {"id": oid, "table": table["table_number"], "status": "new", "total_cents": total_cents})
if path == "/api/merchant-orders":
npub = str(body.get("npub", "")).strip()
request_text = str(body.get("request", "")).strip()[:500]
guest = str(body.get("guest", "")).strip()[:80]
try: table_number = max(1, min(999, int(body.get("table", 1))))
except (TypeError, ValueError): return self.send_json(400, {"error": "enter a valid table number"})
selected = body.get("items") or []
if not request_text and not selected: return self.send_json(400, {"error": "add an order or request"})
try:
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except Exception as error: return self.send_json(502, {"error": f"merchant unavailable: {error}"})
items = [{"id": str(item.get("id", "item"))[:40], "name": str(item.get("name", "Item"))[:80], "qty": max(1, min(20, int(item.get("qty", 1)))), "price_cents": 0} for item in selected[:30]]
if request_text: items.append({"id": "request", "name": request_text, "qty": 1, "price_cents": 0})
oid, timestamp = secrets.token_hex(5), now()
note = f"Guest: {guest}" if guest else ""
with LOCK, db() as conn:
conn.execute("INSERT INTO orders VALUES (?,?,?,?,?,'new',?,?)", (oid, npub, table_number, json.dumps(items), note, timestamp, timestamp))
conn.commit()
return self.send_json(201, {"id": oid, "merchant": nsite.get("name") or place.get("name"), "table": table_number, "status": "new", "created_at": timestamp})
if path == "/api/bookings":
npub = str(body.get("npub", "")).strip()
name = str(body.get("name", "")).strip()[:80]
notes = str(body.get("notes", "")).strip()[:240]
mode = str(body.get("mode", "later"))
try: guests = max(1, min(20, int(body.get("guests", 2))))
except (TypeError, ValueError): return self.send_json(400, {"error": "enter a valid guest count"})
if not name: return self.send_json(400, {"error": "tell the merchant who is booking"})
if mode not in {"now", "later"}: return self.send_json(400, {"error": "choose now or later"})
try:
scheduled = datetime.fromisoformat(str(body.get("scheduled_at", "")).replace("Z", "+00:00"))
if scheduled.tzinfo is None: scheduled = scheduled.replace(tzinfo=timezone.utc)
current = datetime.now(timezone.utc)
if scheduled < current.replace(microsecond=0) and (current - scheduled).total_seconds() > 120: return self.send_json(400, {"error": "choose a future time"})
if scheduled < current: scheduled = current
scheduled_at = scheduled.astimezone(timezone.utc).isoformat(timespec="seconds")
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
except ValueError as error: return self.send_json(400, {"error": str(error) or "choose a valid time"})
except Exception as error: return self.send_json(502, {"error": f"merchant unavailable: {error}"})
booking_id, timestamp = secrets.token_hex(6), now()
with LOCK, db() as conn:
conn.execute("INSERT INTO bookings VALUES (?,?,?,?,?,?,?,'new',?,?)", (booking_id, npub, name, guests, scheduled_at, mode, notes, timestamp, timestamp))
conn.commit()
return self.send_json(201, {"id": booking_id, "merchant": nsite.get("name") or place.get("name"), "name": name, "guests": guests, "scheduled_at": scheduled_at, "status": "new"})
if path == "/api/quick-pay":
npub = str(body.get("npub", "")).strip()
try:
amount_cents = int(body.get("amount_cents", 0))
if amount_cents < 1 or amount_cents > 10_000_000:
raise ValueError("Enter an amount between €0.01 and €100,000")
place, nsite = resolve_merchant_npub(npub)
if not place: return self.send_json(404, {"error": "merchant npub not found"})
lightning_address = str(nsite.get("identity", {}).get("profile", {}).get("lud16") or "").strip()
if not lightning_address: raise ValueError("This merchant has no Lightning address")
prices = fetch_json("https://mempool.space/api/v1/prices")
amount_sats = max(1, round(amount_cents * 1_000_000 / int(prices["EUR"])))
merchant_name = nsite.get("name") or place.get("name")
invoice = lightning_invoice(lightning_address, amount_sats * 1000, str(body.get("comment", "")))
payment_id, created_at = secrets.token_hex(16), now()
with LOCK, db() as conn:
conn.execute("INSERT INTO quick_payments VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
(payment_id, npub, merchant_name, amount_cents, amount_sats, invoice["bolt11"],
invoice["zap_pubkey"], invoice["recipient"], "pending", None, created_at, None))
conn.commit()
qr_svg = helper({"action": "qr", "text": invoice["bolt11"]})["svg"]
return self.send_json(201, {"id": payment_id, "merchant": merchant_name, "amount_cents": amount_cents,
"amount_sats": amount_sats, "bolt11": invoice["bolt11"], "qr_svg": qr_svg,
"status": "pending", "verifiable": invoice["verifiable"], "created_at": created_at})
except ValueError as error: return self.send_json(400, {"error": str(error)})
except HTTPError as error: return self.send_json(502, {"error": f"Lightning provider rejected the request ({error.code})"})
except Exception as error: return self.send_json(502, {"error": f"Invoice unavailable: {error}"})
if path == "/api/payments" and os.environ.get("JWB_ARCHIPELAGO") == "1":
return self.send_json(503, {"error": "Legacy order checkout is not connected to your wallet. Use the merchant website payment page."})
if path == "/api/payments":
token, order_id = str(body.get("token", "")), str(body.get("order_id", ""))
with LOCK, db() as conn:
row = conn.execute("""SELECT o.*,t.token FROM orders o JOIN tables t ON t.merchant_id=o.merchant_id AND t.table_number=o.table_number
WHERE o.id=? AND t.token=?""", (order_id, token)).fetchone()
if not row: return self.send_json(404, {"error": "order not found"})
existing = conn.execute("SELECT * FROM payments WHERE order_id=? ORDER BY created_at DESC LIMIT 1", (order_id,)).fetchone()
if existing and existing["status"] == "pending": return self.send_json(200, dict(existing))
items = json.loads(row["items_json"])
amount_cents = sum(item["price_cents"] * item["qty"] for item in items)
prices = fetch_json("https://mempool.space/api/v1/prices")
amount_sats = max(1, round(amount_cents * 1_000_000 / int(prices["EUR"])))
created_at = now()
zap = helper({"action": "create", "lnurlUrl": LNURL_URL, "relays": PAYMENT_RELAYS, "amountMsat": amount_sats * 1000, "recipient": PAYMENT_RECIPIENT, "table": row["table_number"]})
params = urlencode({"amount": amount_sats * 1000, "nostr": json.dumps(zap["event"], separators=(",", ":")), "lnurl": zap["lnurl"], "comment": f"Just Works Table {row['table_number']}"})
invoice = fetch_json(f"{LNURL_CALLBACK}?{params}")
if invoice.get("status") == "ERROR" or not invoice.get("pr"): return self.send_json(502, {"error": invoice.get("reason", "invoice unavailable")})
payment_id = secrets.token_hex(6)
with LOCK, db() as conn:
conn.execute("INSERT INTO payments VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", (payment_id, order_id, row["merchant_id"], row["table_number"], amount_cents, amount_sats, invoice["pr"], zap["pubkey"], "pending", None, created_at, None))
conn.commit()
return self.send_json(201, {"id": payment_id, "order_id": order_id, "table_number": row["table_number"], "amount_cents": amount_cents, "amount_sats": amount_sats, "bolt11": invoice["pr"], "status": "pending"})
if path.startswith("/api/orders/") and path.endswith("/status"):
oid = path.split("/")[3]
status = str(body.get("status", ""))
if status not in {"new", "accepted", "delivered", "closed"}: return self.send_json(400, {"error": "invalid status"})
with LOCK, db() as conn:
changed = conn.execute("UPDATE orders SET status=?,updated_at=? WHERE id=?", (status, now(), oid)).rowcount
conn.commit()
return self.send_json(200 if changed else 404, {"ok": bool(changed), "status": status})
if path.startswith("/api/payments/") and path.endswith("/acknowledge"):
payment_id = path.split("/")[3]
with LOCK, db() as conn:
changed = conn.execute("UPDATE payments SET status='acknowledged' WHERE id=? AND status='paid'", (payment_id,)).rowcount
conn.commit()
return self.send_json(200 if changed else 404, {"ok": bool(changed), "status": "acknowledged"})
if path.startswith("/api/bookings/") and path.endswith("/status"):
booking_id = path.split("/")[3]
status = str(body.get("status", ""))
if status not in {"new", "confirmed", "seated", "closed", "cancelled"}: return self.send_json(400, {"error": "invalid status"})
with LOCK, db() as conn:
changed = conn.execute("UPDATE bookings SET status=?,updated_at=? WHERE id=?", (status, now(), booking_id)).rowcount
conn.commit()
return self.send_json(200 if changed else 404, {"ok": bool(changed), "status": status})
self.send_error(404)
if __name__ == "__main__":
security.mode()
rollout.policy()
rollout.origins()
with db(): pass
print(f"JustWorks Business listening on http://{HOST}:{PORT}{BASE}/")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()