diff --git a/sites/shkor-com/README.md b/sites/shkor-com/README.md index e49fc5f..bc2647f 100644 --- a/sites/shkor-com/README.md +++ b/sites/shkor-com/README.md @@ -2,6 +2,9 @@ Static page (`index.html`, `styles.css`, `assets/`) + backend `api.py` (stdlib, SQLite): email capture and lab applications with KYC/KYB. +- **Live: https://shkor.com** (since 2026-10-18) — v2 at the root, docroot `/var/www/shkor-com-live` (v2 + legacy team.html/screenshots), Caddy `/etc/caddy/sites/shkor.com.caddy`, apex A → 204.168.232.21 (zone `/etc/bind/zones/db.shkor.com`; www is delegated to namebright). + Old site (nginx on 204.168.138.138, `/var/www/shkor-local`) backed up: `/var/backups/shkor-com/old-shkor-com-site-20261018.tgz` (+ zone, unit, api.py backups there). + Rollback: zone `@` A back to 204.168.138.138, bump serial, `systemctl reload named`. - Preview: https://new.shkor.com (noindex) — docroot `/var/www/shkor-com`, Caddy `/etc/caddy/sites/new-shkor.caddy` - Video `assets/simbiot-harness-enterprise-en.mp4` is copied from `out/` at deploy (not committed here) - Backend: `systemd shkor-api` (/opt/shkor-subscribe/api.py) on 127.0.0.1:8097, Caddy proxies `/api/*`; DB `/var/lib/shkor-subscribe/subs.db` @@ -12,5 +15,6 @@ Static page (`index.html`, `styles.css`, `assets/`) + backend `api.py` (stdlib, After publishing v2 at the root, set `PUBLIC_BASE=https://shkor.com` (Didit callback). export: `sqlite3 /var/lib/shkor-subscribe/subs.db "select email,source,created_at from subs"` - v2 (labs + autopilot concept): `v2/` → https://new.shkor.com/v2/ ; design direction from ui-ux-pro-max (Dark OLED + Bento + HUD, Space Grotesk) -- Update: `cp -r index.html styles.css assets /var/www/shkor-com/` (v2: `cp -r v2/* /var/www/shkor-com/v2/`) -- Apex shkor.com still points at 204.168.138.138 (old site) — switch A record after review. +- Update live: `cp -r v2/* /var/www/shkor-com-live/ && cp api.py /opt/shkor-subscribe/ && systemctl restart shkor-api` +- Autopilot $99 (`v2/start/`): form → Didit KYC → `/start/status/?t=` → after Approved, Stripe Checkout via portal onboarding (plan `autopilot`, return_url back here) → portal provisions tenant + Hetzner VM + agent → cloud-init callback → CloudCLI panel `.ui.shkor.com` with Claude Code + OpenCode (GLM via 9router) → status page shows panel login. + Stripe mode follows the portal's `STRIPE_MODE` (/root/coop/.env). Test: `python3 sites/shkor-com/test_autopilot.py`. diff --git a/sites/shkor-com/api.py b/sites/shkor-com/api.py index 9dced38..91613a5 100644 --- a/sites/shkor-com/api.py +++ b/sites/shkor-com/api.py @@ -4,6 +4,9 @@ POST /api/subscribe form email -> subs POST /api/lab/apply JSON -> portal apply pipeline (42.cil.by /api/studio/apply) + Didit sessions GET /api/lab/status?t= application + verification status (refreshed from Didit) + POST /api/autopilot/start JSON name/email -> Didit KYC session (Autopilot $99, shkor.com/start) + GET /api/autopilot/status?t= stage: kyc -> pay -> deploying -> ready (panel url + login) + POST /api/autopilot/checkout JSON t -> Stripe Checkout via portal onboarding (only after KYC Approved) Env: SHKOR_DB sqlite path (default /var/lib/shkor-subscribe/subs.db) @@ -31,6 +34,8 @@ TG_CHAT = os.environ.get("TG_CHAT_ID", "7569223131") FINAL = {"Approved", "Declined", "Abandoned", "Expired"} EMAIL_RE = re.compile(r"^[^@\s]{1,64}@[^@\s]+\.[A-Za-z]{2,}$") RATE = {} # (bucket, ip) -> [timestamps] +# портал провижнит на первом опросе после оплаты — не опрашивать параллельно (двойной тенант) +AP_LOCK = threading.Lock() def env_value(path, key): @@ -60,6 +65,11 @@ def db(): name text, email text, project_name text, company_name text, company_country text, company_reg text, ip text, created_at text default (datetime('now'))); + create table if not exists ap_orders ( + token text primary key, name text, email text, company text, ip text, + kyc_session text, kyc_url text, kyc_status text, kyc_notified text default '', + pending_id text default '', paid_notified integer default 0, + created_at text default (datetime('now')), updated_at text default (datetime('now'))); create table if not exists lab_checks ( token text, kind text, session_id text, url text, status text, notified text default '', updated_at text default (datetime('now')), @@ -127,7 +137,7 @@ def refresh(c, token, force=False): def poller(): while True: - time.sleep(600) + time.sleep(300) try: c = db() toks = [r[0] for r in c.execute( @@ -135,7 +145,13 @@ def poller(): "and updated_at > datetime('now','-30 days')")] for t in toks: refresh(c, t) + # Autopilot: KYC без открытой вкладки + провижн после оплаты, даже если вкладку закрыли + aps = [r[0] for r in c.execute( + "select token from ap_orders where paid_notified=0 and created_at > datetime('now','-7 days') " + "and (pending_id!='' or kyc_status not in ('Approved','Declined','Abandoned','Expired'))")] c.close() + for t in aps: + ap_status(t) except Exception: pass @@ -211,6 +227,127 @@ def lab_apply(d, ip): return 200, {"ok": True, "token": token, "status_url": f"{PUBLIC_BASE}/apply/status/?t={token}", "kyc_url": kyc_url} +def portal(path, data=None, timeout=30): + """Вызов портала (onboarding/Stripe/провижн) напрямую, с Host основного сайта.""" + req = urllib.request.Request(PORTAL_URL + path, data=json.dumps(data).encode() if data is not None else None, + headers={"Content-Type": "application/json", "Host": "cil.by"}, + method="POST" if data is not None else "GET") + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + + +def ap_refresh(c, o): + """KYC-статус из Didit (пока не финальный); уведомление админу один раз.""" + st = o["kyc_status"] + if o["kyc_session"] and st not in FINAL: + try: + st = didit_status(o["kyc_session"]) or st + except Exception: + return o + c.execute("update ap_orders set kyc_status=?, updated_at=datetime('now') where token=?", (st, o["token"])) + if st in FINAL and o["kyc_notified"] != st: + tg(f"{'✅' if st == 'Approved' else '⛔'} KYC {st} — Autopilot $99\n👤 {o['name']} · {o['email']}" + + (f"\n🏢 {o['company']}" if o["company"] else "")) + c.execute("update ap_orders set kyc_notified=? where token=?", (st, o["token"])) + c.commit() + o = c.execute("select * from ap_orders where token=?", (o["token"],)).fetchone() + return o + + +def ap_start(d, ip): + if clean(d, "company_hp"): # honeypot + return 200, {"ok": True, "token": "x" * 16} + if limited("ap", ip, 5): + return 429, {"error": "Too many attempts from this network — try again in an hour."} + name, email, company = clean(d, "name", 120), clean(d, "email", 254).lower(), clean(d, "company", 160) + if not name: + return 400, {"error": "Please enter your full name (as in your ID)."} + if not EMAIL_RE.match(email): + return 400, {"error": "Please enter a valid email."} + if not d.get("consent"): + return 400, {"error": "Please accept the terms and identity verification."} + token = secrets.token_urlsafe(16) + try: + s = didit_session(KYC_WF, f"shkor-ap:{token}", f"{PUBLIC_BASE}/start/status/?t={token}") + except Exception: + return 502, {"error": "Identity verification is unavailable right now. Please try again in a few minutes."} + c = db() + c.execute("insert into ap_orders(token, name, email, company, ip, kyc_session, kyc_url, kyc_status) " + "values (?,?,?,?,?,?,?,?)", (token, name, email, company, ip, s.get("session_id", ""), + s.get("url", ""), s.get("status", "Not Started"))) + c.commit() + c.close() + tg(f"🆕 Autopilot $99 — начат KYC\n👤 {name} · {email}" + (f"\n🏢 {company}" if company else "")) + return 200, {"ok": True, "token": token, "kyc_url": s.get("url", ""), + "status_url": f"{PUBLIC_BASE}/start/status/?t={token}"} + + +def ap_status(token): + c = db() + o = c.execute("select * from ap_orders where token=?", (token,)).fetchone() + if not o: + c.close() + return 404, {"error": "Order not found."} + o = ap_refresh(c, o) + out = {"ok": True, "name": o["name"], "email": o["email"], "kyc": o["kyc_status"]} + if o["kyc_status"] != "Approved": + out["stage"] = "declined" if o["kyc_status"] in FINAL else "kyc" + out["kyc_url"] = "" if o["kyc_status"] in FINAL else o["kyc_url"] + elif not o["pending_id"]: + out["stage"] = "pay" + else: + try: + with AP_LOCK: + r = portal(f"/api/onboarding/status?pending_id={o['pending_id']}", timeout=120) + except Exception: + r = {"status": "pending"} + if r.get("status") != "ready": + out["stage"] = "pay" + else: + if not o["paid_notified"]: + tg(f"💳 Autopilot $99 оплачен\n👤 {o['name']} · {o['email']}\n🧩 тенант {r.get('tenant_slug')}") + c.execute("update ap_orders set paid_notified=1 where token=?", (token,)) + c.commit() + panel = r.get("panel") + out["stage"] = "ready" if panel else "deploying" + out["agent"] = r.get("agent") + out["portal_url"] = r.get("portal_url", "") + out["panel"] = panel + c.close() + return 200, out + + +def ap_checkout(d): + token = clean(d, "t", 64) + c = db() + o = c.execute("select * from ap_orders where token=?", (token,)).fetchone() + if not o: + c.close() + return 404, {"error": "Order not found."} + o = ap_refresh(c, o) + if o["kyc_status"] != "Approved": + c.close() + return 403, {"error": "Payment opens after your identity is verified."} + try: + pid = o["pending_id"] + if not pid: + pid = portal("/api/onboarding/start", {"email": o["email"], "plan": "autopilot"})["pending_id"] + c.execute("update ap_orders set pending_id=? where token=?", (pid, token)) + c.commit() + label = re.sub(r"[^a-z0-9]+", "-", (o["company"] or o["name"]).lower()).strip("-")[:16] or "autopilot" + portal("/api/onboarding/save", {"pending_id": pid, "company_name": o["company"] or o["name"], + "agent_name": f"{label}-{secrets.token_hex(2)}"}) + r = portal("/api/onboarding/checkout", {"pending_id": pid, "plan": "autopilot", + "return_url": f"{PUBLIC_BASE}/start/status/?t={token}"}) + except Exception: + c.close() + return 502, {"error": "Couldn't open the payment page. Please try again."} + c.close() + if not r.get("url"): + return 502, {"error": r.get("error") or "Couldn't open the payment page."} + return 200, {"ok": True, "url": r["url"]} + + def lab_status(token): c = db() app = c.execute("select * from lab_apps where token=?", (token,)).fetchone() @@ -253,6 +390,11 @@ class H(BaseHTTPRequestHandler): if not re.fullmatch(r"[A-Za-z0-9_-]{10,64}", t): return self.reply(400, {"error": "Bad link."}) return self.reply(*lab_status(t)) + if u.path.rstrip("/") == "/api/autopilot/status": + t = (parse_qs(u.query).get("t") or [""])[0] + if not re.fullmatch(r"[A-Za-z0-9_-]{10,64}", t): + return self.reply(400, {"error": "Bad link."}) + return self.reply(*ap_status(t)) self.reply(404, {"error": "not found"}) def do_POST(self): @@ -266,6 +408,12 @@ class H(BaseHTTPRequestHandler): except ValueError: return self.reply(400, {"error": "bad json"}) return self.reply(*lab_apply(d, self.ip())) + if path in ("/api/autopilot/start", "/api/autopilot/checkout"): + try: + d = json.loads(raw or "{}") + except ValueError: + return self.reply(400, {"error": "bad json"}) + return self.reply(*(ap_start(d, self.ip()) if path.endswith("start") else ap_checkout(d))) if path != "/api/subscribe": return self.reply(404, {"error": "not found"}) form = {k: v[0] for k, v in parse_qs(raw).items()} diff --git a/sites/shkor-com/test_autopilot.py b/sites/shkor-com/test_autopilot.py new file mode 100644 index 0000000..93ce7af --- /dev/null +++ b/sites/shkor-com/test_autopilot.py @@ -0,0 +1,62 @@ +"""Тест флоу Autopilot $99 (shkor.com/start): KYC → оплата → деплой. Портал и Didit — фейки. + python3 sites/shkor-com/test_autopilot.py +""" +import json +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlsplit, parse_qs +S={"calls":[], "paid":False} +class H(BaseHTTPRequestHandler): + def r(s,o): + b=json.dumps(o).encode(); s.send_response(200); s.send_header("Content-Type","application/json"); s.end_headers(); s.wfile.write(b) + def do_POST(s): + d=json.loads(s.rfile.read(int(s.headers["Content-Length"]))) + p=urlsplit(s.path).path; S["calls"].append((p,d)) + if p=="/api/onboarding/start": return s.r({"pending_id":"pid123"}) + if p=="/api/onboarding/save": return s.r({"ok":True}) + if p=="/api/onboarding/checkout": return s.r({"url":"https://checkout.stripe.test/x"}) + if p=="/__pay": S["paid"]=True; return s.r({}) + if p=="/__calls": return s.r(S["calls"]) + def do_GET(s): + if S["paid"]: return s.r({"status":"ready","tenant_slug":"t1","portal_url":"https://t1.cil.by","agent":{"name":"a","status":"running"},"panel":{"url":"https://a.ui.shkor.com/","user":"a","password":"p"}}) + return s.r({"status":"pending"}) + def log_message(s,*a): pass + + +import os, sys, sqlite3, importlib.util, threading +threading.Thread(target=ThreadingHTTPServer(("127.0.0.1",18899),H).serve_forever,daemon=True).start() +import tempfile +TMP=tempfile.mkdtemp() +os.environ.update(SHKOR_DB=TMP+"/t.db", PORTAL_URL="http://127.0.0.1:18899", TG_ENV_FILE="/nonexistent") +spec=importlib.util.spec_from_file_location("api",os.path.join(os.path.dirname(os.path.abspath(__file__)),"api.py")); api=importlib.util.module_from_spec(spec); spec.loader.exec_module(api) +st={"s":"Not Started"} +api.didit_session=lambda wf,v,cb: {"session_id":"sid1","url":"https://verify.didit.me/x","status":"Not Started"} +api.didit_status=lambda sid: st["s"] +ok=0; bad=0 +def eq(a,b,m): + global ok,bad + if a==b: ok+=1 + else: bad+=1; print("FAIL",m,a,"!=",b) +code,r=api.ap_start({"name":"","email":"x@y.com","consent":True},"1.1.1.1"); eq(code,400,"name required") +code,r=api.ap_start({"name":"A B","email":"bad","consent":True},"1.1.1.1"); eq(code,400,"email") +code,r=api.ap_start({"name":"A B","email":"a@b.com"},"1.1.1.1"); eq(code,400,"consent") +code,r=api.ap_start({"name":"Ann Bee","email":"A@B.com","company":"Acme Co","consent":True},"1.1.1.1"); eq(code,200,"start"); t=r["token"] +eq(r["kyc_url"],"https://verify.didit.me/x","kyc url") +code,r=api.ap_status(t); eq(r["stage"],"kyc","stage kyc") +code,r=api.ap_checkout({"t":t}); eq(code,403,"checkout blocked before kyc") +st["s"]="Declined"; code,r=api.ap_status(t); eq(r["stage"],"declined","declined"); eq(r["kyc_url"],"","no url when final") +# new order approved +st["s"]="Not Started"; code,r=api.ap_start({"name":"Ann Bee","email":"a@b.com","company":"Acme Co","consent":True},"2.2.2.2"); t=r["token"] +st["s"]="Approved"; code,r=api.ap_status(t); eq(r["stage"],"pay","pay stage") +code,r=api.ap_checkout({"t":t}); eq(code,200,"checkout ok"); eq(r["url"],"https://checkout.stripe.test/x","stripe url") +code,r=api.ap_checkout({"t":t}); eq(code,200,"checkout again reuses pending") +code,r=api.ap_status(t); eq(r["stage"],"pay","still pay before payment") +import urllib.request +def call(p): return json.loads(urllib.request.urlopen(urllib.request.Request("http://127.0.0.1:18899"+p,data=b"{}",headers={"Content-Type":"application/json"})).read()) +calls=call("/__calls"); starts=[c for c in calls if c[0]=="/api/onboarding/start"]; eq(len(starts),1,"one pending created") +eq(starts[0][1],{"email":"a@b.com","plan":"autopilot"},"plan autopilot") +co=[c for c in calls if c[0]=="/api/onboarding/checkout"][0][1]; eq(co["return_url"],f"https://shkor.com/start/status/?t={t}","return url") +sv=[c for c in calls if c[0]=="/api/onboarding/save"][0][1]; eq(sv["agent_name"].startswith("acme-co-"),True,"agent name") +call("/__pay"); code,r=api.ap_status(t); eq(r["stage"],"ready","ready"); eq(r["panel"]["user"],"a","panel creds") +c=sqlite3.connect(TMP+"/t.db"); eq(c.execute("select paid_notified from ap_orders where token=?",(t,)).fetchone()[0],1,"paid flag") +code,r=api.ap_status("nope-nope-nope"); eq(code,404,"404") +print(f"{ok} ok, {bad} fail"); sys.exit(1 if bad else 0) diff --git a/sites/shkor-com/v2/index.html b/sites/shkor-com/v2/index.html index eebb5b5..8f57ee2 100644 --- a/sites/shkor-com/v2/index.html +++ b/sites/shkor-com/v2/index.html @@ -242,10 +242,10 @@

For companies

Put your business on autopilot

-

Your own Simbiot — the same system our labs run in production, not a cut-down demo. Deployment and onboarding within the month you join; LLM access through the cooperative's router included.

-

$100/month

-

One price for everyone. 10 presale seats.

- Reserve a seat +

Your own server with a web panel: Claude Code and OpenCode to build with, and a Simbiot agent — the same system our labs run in production. Verify, pay, and it deploys itself.

+

$99/month

+

One price for everyone. Cancel any time.

+ Start now

For founders

diff --git a/sites/shkor-com/v2/start/index.html b/sites/shkor-com/v2/start/index.html new file mode 100644 index 0000000..0274f95 --- /dev/null +++ b/sites/shkor-com/v2/start/index.html @@ -0,0 +1,84 @@ + + + + + + + +Start your autopilot — Shkor Labs + + + + + + + +
+ +
+ + +
+
+
+ Step 1 of 3About you +
+
Your workspace login and receipts go here.
+
+ + +
+ +
+

Payment opens right after verification. Nothing is charged before that.

+
+
+
+
+ + + diff --git a/sites/shkor-com/v2/start/status/index.html b/sites/shkor-com/v2/start/status/index.html new file mode 100644 index 0000000..63c4094 --- /dev/null +++ b/sites/shkor-com/v2/start/status/index.html @@ -0,0 +1,96 @@ + + + + + + + +Your workspace — Shkor Labs + + + + + + + + +
+ +
+

Autopilot · $99/month

+

Loading…

+

+
    +
    + +

    Bookmark this page. It's your private link: come back any time to finish a step or find your workspace login. Don't share it.

    +
    +
    +
    + + +