top-right LOG IN/SIGN UP on every page + /signup quick page + GET /logout
- page() acct slot now always filled: LOG IN + SIGN UP buttons (logged out), balance chip + EXIT (logged in)
- fix: {{acct}} was Jinja-autoescaped to literal text (now Markup)
- fix: missing + in chip concat raised inside silent except - chip never rendered
- login/register honor ?next= (same-site only); signup wired into hero, drawer, sitemap, openapi, llms.txt
- verified E2E: register->\$1 balance->chip->logout; 24 routes 200; visual QA home+signup
This commit is contained in:
408
app.py
408
app.py
@@ -223,10 +223,10 @@ BASE = """<!doctype html><html lang=en><head><meta charset=utf-8><meta name=view
|
|||||||
:root{--bg:#070a13;--card:rgba(19,25,44,.82);--line:#242e4d;--fg:#e9edf8;--dim:#93a0c2;--acc:#a78bfa;--acc2:#6fd6ff;--acc3:#6fd6ff;--ok:#42e8a4;--bad:#ff6161}
|
:root{--bg:#070a13;--card:rgba(19,25,44,.82);--line:#242e4d;--fg:#e9edf8;--dim:#93a0c2;--acc:#a78bfa;--acc2:#6fd6ff;--acc3:#6fd6ff;--ok:#42e8a4;--bad:#ff6161}
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
html{scroll-behavior:smooth}
|
html{scroll-behavior:smooth}
|
||||||
body{margin:0;min-height:100vh;color:var(--fg);font:16px/1.6 ui-monospace,Menlo,Consolas,monospace;text-align:center;overflow-x:hidden;background:var(--bg)}
|
body{margin:0;min-height:100vh;color:var(--fg);font:16.5px/1.7 ui-monospace,Menlo,Consolas,monospace;text-align:left;overflow-x:hidden;background:var(--bg)}
|
||||||
#space{position:fixed;inset:0;z-index:0;display:block}
|
#space{position:fixed;inset:0;z-index:0;display:block}
|
||||||
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;background:radial-gradient(ellipse at 50% -10%,var(--neb1,rgba(120,85,255,.16)),transparent 55%),radial-gradient(ellipse at 80% 110%,var(--neb2,rgba(0,190,255,.10)),transparent 50%),radial-gradient(ellipse at 50% 50%,transparent 60%,rgba(0,0,5,.55) 100%)}
|
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;background:radial-gradient(ellipse at 50% -10%,var(--neb1,rgba(120,85,255,.16)),transparent 55%),radial-gradient(ellipse at 80% 110%,var(--neb2,rgba(0,190,255,.10)),transparent 50%),radial-gradient(ellipse at 50% 50%,transparent 60%,rgba(0,0,5,.55) 100%)}
|
||||||
main{position:relative;z-index:2;max-width:1060px;margin:0 auto;padding:1.6rem 1.1rem 4rem;text-align:center}
|
main{position:relative;z-index:2;max-width:920px;margin:0 auto;padding:1.8rem 1.3rem 4rem;text-align:left}
|
||||||
#lbar{position:fixed;top:0;left:0;height:3px;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));z-index:50;transition:width .3s;box-shadow:0 0 10px var(--acc)}
|
#lbar{position:fixed;top:0;left:0;height:3px;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));z-index:50;transition:width .3s;box-shadow:0 0 10px var(--acc)}
|
||||||
#lbar.done{width:100%;opacity:0;transition:opacity .5s}
|
#lbar.done{width:100%;opacity:0;transition:opacity .5s}
|
||||||
header{position:sticky;top:0;z-index:40;background:rgba(7,10,19,.86);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
header{position:sticky;top:0;z-index:40;background:rgba(7,10,19,.86);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
|
||||||
@@ -234,8 +234,8 @@ header{position:sticky;top:0;z-index:40;background:rgba(7,10,19,.86);backdrop-fi
|
|||||||
.logo{color:var(--acc);text-decoration:none;font-weight:800;letter-spacing:.28em;font-size:1rem;text-shadow:0 0 18px rgba(167,139,250,.4)}
|
.logo{color:var(--acc);text-decoration:none;font-weight:800;letter-spacing:.28em;font-size:1rem;text-shadow:0 0 18px rgba(167,139,250,.4)}
|
||||||
.burger{background:none;border:1px solid var(--line);color:var(--fg);font-size:1.15rem;border-radius:8px;padding:.35rem .7rem;cursor:pointer}
|
.burger{background:none;border:1px solid var(--line);color:var(--fg);font-size:1.15rem;border-radius:8px;padding:.35rem .7rem;cursor:pointer}
|
||||||
.burger:hover{border-color:var(--acc);color:var(--acc)}
|
.burger:hover{border-color:var(--acc);color:var(--acc)}
|
||||||
.dnav{display:flex;flex-wrap:wrap;gap:.35rem;justify-content:center}
|
.dnav{display:flex;flex-wrap:wrap;gap:.5rem;justify-content:flex-start}
|
||||||
.dnav a{color:var(--dim);text-decoration:none;font-size:.72rem;padding:.3rem .55rem;border:1px solid var(--line);border-radius:999px;white-space:nowrap;transition:.15s}
|
.dnav a{color:var(--dim);text-decoration:none;font-size:.84rem;padding:.45rem .8rem;border:1px solid var(--line);border-radius:999px;white-space:nowrap;transition:.15s}
|
||||||
.dnav a.on,.dnav a:hover{color:var(--acc);border-color:var(--acc)}
|
.dnav a.on,.dnav a:hover{color:var(--acc);border-color:var(--acc)}
|
||||||
.tchip{color:var(--fg);margin-right:.5rem;white-space:nowrap}
|
.tchip{color:var(--fg);margin-right:.5rem;white-space:nowrap}
|
||||||
.srow{display:flex;align-items:center;gap:.5rem;margin:.45rem 0;font-size:.85rem;color:var(--fg)}
|
.srow{display:flex;align-items:center;gap:.5rem;margin:.45rem 0;font-size:.85rem;color:var(--fg)}
|
||||||
@@ -260,21 +260,25 @@ table{display:block;overflow-x:auto;max-width:100%;-webkit-overflow-scrolling:to
|
|||||||
.drawer a.dl small{display:block;color:var(--dim);font-size:.72rem}
|
.drawer a.dl small{display:block;color:var(--dim);font-size:.72rem}
|
||||||
@media(min-width:860px){.burger{display:none}}
|
@media(min-width:860px){.burger{display:none}}
|
||||||
@media(max-width:859px){.dnav{display:none}}
|
@media(max-width:859px){.dnav{display:none}}
|
||||||
h1{font-size:1.55rem;letter-spacing:.18em;margin:.8rem 0 .2rem}
|
h1{font-size:1.9rem;letter-spacing:.04em;margin:1.3rem 0 .35rem;line-height:1.25}
|
||||||
h1 span{color:var(--acc)}
|
h1 span{color:var(--acc)}
|
||||||
.sub{color:var(--dim);margin:.2rem 0 1.4rem;font-size:.95rem}
|
.sub{color:var(--dim);margin:.25rem 0 1.7rem;font-size:1.03rem;line-height:1.65;max-width:62ch}
|
||||||
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:1.15rem 1.2rem;margin:.9rem 0;backdrop-filter:blur(4px)}
|
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:1.4rem 1.5rem;margin:1.35rem 0;backdrop-filter:blur(4px)}
|
||||||
.card.glow{box-shadow:0 0 34px -16px var(--acc)}
|
.card.glow{box-shadow:0 0 34px -16px var(--acc)}
|
||||||
.kv{display:grid;grid-template-columns:1fr;gap:.25rem;text-align:left}
|
.kv{display:grid;grid-template-columns:1fr;gap:.45rem .9rem;text-align:left}
|
||||||
.kv div:nth-child(odd){color:var(--dim);font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;padding-top:.45rem}
|
.kv div:nth-child(odd){color:var(--dim);font-size:.82rem;letter-spacing:.05em;text-transform:uppercase;padding-top:.5rem;font-weight:700}
|
||||||
.kv div:nth-child(even){background:rgba(255,255,255,.03);border-radius:8px;padding:.35rem .6rem}
|
.kv div:nth-child(even){background:rgba(255,255,255,.03);border-radius:8px;padding:.35rem .6rem}
|
||||||
@media(min-width:640px){.kv{grid-template-columns:180px 1fr}.kv div:nth-child(odd){padding-top:.35rem}}
|
@media(min-width:640px){.kv{grid-template-columns:180px 1fr}.kv div:nth-child(odd){padding-top:.35rem}}
|
||||||
input,select,button,textarea{font:inherit;background:rgba(10,15,30,.9);color:var(--fg);border:1px solid #2c3860;border-radius:10px;padding:.6rem .8rem;max-width:100%}
|
input,select,button,textarea{font:inherit;background:rgba(10,15,30,.9);color:var(--fg);border:1px solid #2c3860;border-radius:10px;padding:.72rem .9rem;max-width:100%;text-align:left}
|
||||||
|
.card label{display:block;text-align:left;color:var(--dim);font-size:.88rem;margin:.6rem 0 .2rem}
|
||||||
|
.card input,.card select,.card textarea{width:100%}
|
||||||
|
.card form{text-align:left}
|
||||||
|
.card p{text-align:left;line-height:1.7}
|
||||||
button{background:linear-gradient(135deg,var(--acc),var(--acc2));color:#0d0722;border:0;font-weight:800;cursor:pointer;transition:.2s;letter-spacing:.05em}
|
button{background:linear-gradient(135deg,var(--acc),var(--acc2));color:#0d0722;border:0;font-weight:800;cursor:pointer;transition:.2s;letter-spacing:.05em}
|
||||||
button:hover{filter:brightness(1.15);box-shadow:0 0 20px -4px var(--acc)}
|
button:hover{filter:brightness(1.15);box-shadow:0 0 20px -4px var(--acc)}
|
||||||
button.ghost{background:transparent;color:var(--acc);border:1px solid var(--acc)}
|
button.ghost{background:transparent;color:var(--acc);border:1px solid var(--acc)}
|
||||||
button.big{font-size:1.02rem;padding:.75rem 1.4rem;margin:.25rem;color:#0d0722}
|
button.big{font-size:1.02rem;padding:.75rem 1.4rem;margin:.25rem;color:#0d0722}
|
||||||
.grid2{display:grid;grid-template-columns:1fr;gap:.9rem;text-align:center}
|
.grid2{display:grid;grid-template-columns:1fr;gap:1.25rem;text-align:left}
|
||||||
@media(min-width:700px){.grid2{grid-template-columns:repeat(2,1fr);gap:1.1rem}}
|
@media(min-width:700px){.grid2{grid-template-columns:repeat(2,1fr);gap:1.1rem}}
|
||||||
.grid3{display:grid;grid-template-columns:1fr;gap:1.1rem;text-align:left}
|
.grid3{display:grid;grid-template-columns:1fr;gap:1.1rem;text-align:left}
|
||||||
@media(min-width:700px){.grid3{grid-template-columns:repeat(2,1fr)}}
|
@media(min-width:700px){.grid3{grid-template-columns:repeat(2,1fr)}}
|
||||||
@@ -301,10 +305,10 @@ button.big{font-size:1.02rem;padding:.75rem 1.4rem;margin:.25rem;color:#0d0722}
|
|||||||
@media(min-width:700px){.grid2{grid-template-columns:1fr 1fr}}
|
@media(min-width:700px){.grid2{grid-template-columns:1fr 1fr}}
|
||||||
.tag{display:inline-block;padding:.14rem .6rem;border-radius:999px;font-size:.74rem;border:1px solid;margin:.15rem}
|
.tag{display:inline-block;padding:.14rem .6rem;border-radius:999px;font-size:.74rem;border:1px solid;margin:.15rem}
|
||||||
.tag.ok{color:var(--ok);border-color:var(--ok)}.tag.bad{color:var(--bad);border-color:var(--bad)}.tag.warn{color:var(--acc);border-color:var(--acc)}
|
.tag.ok{color:var(--ok);border-color:var(--ok)}.tag.bad{color:var(--bad);border-color:var(--bad)}.tag.warn{color:var(--acc);border-color:var(--acc)}
|
||||||
table{width:100%;border-collapse:collapse;font-size:.85rem}
|
table{width:100%;border-collapse:collapse;font-size:.93rem}
|
||||||
td,th{padding:.4rem;border-bottom:1px solid var(--line);text-align:left}
|
td,th{padding:.55rem .5rem;border-bottom:1px solid var(--line);text-align:left}
|
||||||
th{color:var(--dim);text-transform:uppercase;font-size:.7rem;letter-spacing:.14em}
|
th{color:var(--dim);text-transform:uppercase;font-size:.75rem;letter-spacing:.06em}
|
||||||
footer{color:var(--dim);padding:2.2rem 1rem 5rem;font-size:.8rem;position:relative;z-index:2;text-align:center}
|
footer{color:var(--dim);padding:2.4rem 1rem 5rem;font-size:.88rem;position:relative;z-index:2;text-align:center;line-height:1.7}
|
||||||
footer a{color:var(--acc)}
|
footer a{color:var(--acc)}
|
||||||
code{background:rgba(10,15,30,.9);padding:.08rem .4rem;border-radius:5px;font-size:.86em;word-break:break-all}
|
code{background:rgba(10,15,30,.9);padding:.08rem .4rem;border-radius:5px;font-size:.86em;word-break:break-all}
|
||||||
a{color:var(--acc2)}
|
a{color:var(--acc2)}
|
||||||
@@ -320,7 +324,14 @@ pre{text-align:left;white-space:pre-wrap;overflow-x:auto}
|
|||||||
img{max-width:100%;border-radius:10px}
|
img{max-width:100%;border-radius:10px}
|
||||||
li{text-align:left;margin:.2rem 0}
|
li{text-align:left;margin:.2rem 0}
|
||||||
.gridlines{position:fixed;inset:0;z-index:1;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.012) 0 1px,transparent 1px 3px),linear-gradient(rgba(111,214,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(111,214,255,.03) 1px,transparent 1px);background-size:auto,80px 80px,80px 80px;mask-image:linear-gradient(rgba(0,0,0,.7),rgba(0,0,0,.25))}
|
.gridlines{position:fixed;inset:0;z-index:1;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.012) 0 1px,transparent 1px 3px),linear-gradient(rgba(111,214,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(111,214,255,.03) 1px,transparent 1px);background-size:auto,80px 80px,80px 80px;mask-image:linear-gradient(rgba(0,0,0,.7),rgba(0,0,0,.25))}
|
||||||
</style></head><body>
|
#acct{display:flex;align-items:center;gap:.4rem;white-space:nowrap}
|
||||||
|
.abtn{display:inline-flex;align-items:center;font-size:.78rem;letter-spacing:.08em;text-decoration:none;border-radius:999px;padding:.42rem .95rem;border:1px solid var(--line);color:var(--dim);transition:.15s;cursor:pointer}
|
||||||
|
.abtn:hover{color:var(--acc);border-color:var(--acc);box-shadow:0 0 14px -6px var(--acc)}
|
||||||
|
.abtn.solid{background:linear-gradient(135deg,var(--acc),var(--acc2));color:#0d0722;border-color:transparent;font-weight:800}
|
||||||
|
.abtn.solid:hover{filter:brightness(1.15);color:#0d0722}
|
||||||
|
.achip{display:inline-flex;align-items:center;gap:.3rem;font-size:.72rem;color:var(--acc2);text-decoration:none;border:1px solid var(--line);border-radius:999px;padding:.3rem .7rem}
|
||||||
|
.achip:hover{border-color:var(--acc2)}
|
||||||
|
</style><script src="https://supernova.thetempleofdoom.com/static/cosmic-bg.js?v=3" data-theme="lissajous" data-interact="parallax" data-accent="#a06bff"></script></head><body>
|
||||||
<div id="lbar"></div>
|
<div id="lbar"></div>
|
||||||
<a class=skip href="#main">skip to content</a>
|
<a class=skip href="#main">skip to content</a>
|
||||||
<canvas id="space"></canvas><div class="gridlines"></div><div class="vignette" style="--neb1:{{n1}};--neb2:{{n2}}"></div>
|
<canvas id="space"></canvas><div class="gridlines"></div><div class="vignette" style="--neb1:{{n1}};--neb2:{{n2}}"></div>
|
||||||
@@ -334,7 +345,7 @@ li{text-align:left;margin:.2rem 0}
|
|||||||
<a href=/canary class={{o('canary')}}>CANARY</a><a href=/deaddrop class={{o('deaddrop')}}>DEAD-DROP</a><a href=/mail class={{o('mail')}}>BURNER-MAIL</a>
|
<a href=/canary class={{o('canary')}}>CANARY</a><a href=/deaddrop class={{o('deaddrop')}}>DEAD-DROP</a><a href=/mail class={{o('mail')}}>BURNER-MAIL</a>
|
||||||
<a href=/shot class={{o('shot')}}>SHOT</a><a href=/score class={{o('score')}}>FRAUD-SCORE</a>
|
<a href=/shot class={{o('shot')}}>SHOT</a><a href=/score class={{o('score')}}>FRAUD-SCORE</a>
|
||||||
<a href=/inbox class={{o('inbox')}}>INBOX</a><a href=/passport class={{o('passport')}}>PASSPORT</a>
|
<a href=/inbox class={{o('inbox')}}>INBOX</a><a href=/passport class={{o('passport')}}>PASSPORT</a>
|
||||||
<a href=/pass class={{o('pass')}}>PASS</a><a href=/keys class={{o('keys')}}>KEYS</a><a href=/tools class={{o('tools')}}>TOOLS</a>
|
<a href=/pass class={{o('pass')}}>PASS</a><a href=/keys class={{o('keys')}}>KEYS</a><a href=/maglab class={{o('maglab')}}>MAG-LAB</a><a href=/tools class={{o('tools')}}>TOOLS</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="acct">{{acct}}</div><button class=burger id=burger onclick="drw()">☰</button>
|
<div id="acct">{{acct}}</div><button class=burger id=burger onclick="drw()">☰</button>
|
||||||
</div></header>
|
</div></header>
|
||||||
@@ -343,6 +354,7 @@ li{text-align:left;margin:.2rem 0}
|
|||||||
<h4>Intel</h4>
|
<h4>Intel</h4>
|
||||||
<a class="dl {{o('ip')}}" href=/ip>◈ IP INTEL <small>geo, ASN, ISP, VPN flags — any target</small></a>
|
<a class="dl {{o('ip')}}" href=/ip>◈ IP INTEL <small>geo, ASN, ISP, VPN flags — any target</small></a>
|
||||||
<a class="dl {{o('card')}}" href=/card>◈ CARD CHECK <small>luhn + BIN issuer intelligence</small></a>
|
<a class="dl {{o('card')}}" href=/card>◈ CARD CHECK <small>luhn + BIN issuer intelligence</small></a>
|
||||||
|
<a class="dl {{o('maglab')}}" href=/maglab>◈ MAG-LAB <small>browser magstripe studio — ISO 7811 encode, read, batch issue (closed-loop only)</small></a>
|
||||||
<a class="dl {{o('eh')}}" href=/eh>◈ MAIL FORENSICS <small>origin + SPF/DKIM/DMARC + spoof flags</small></a>
|
<a class="dl {{o('eh')}}" href=/eh>◈ MAIL FORENSICS <small>origin + SPF/DKIM/DMARC + spoof flags</small></a>
|
||||||
<a class="dl {{o('forensics')}}" href=/forensics>◈ IMAGE FORENSICS <small>EXIF, GPS, ELA, edit detection</small></a>
|
<a class="dl {{o('forensics')}}" href=/forensics>◈ IMAGE FORENSICS <small>EXIF, GPS, ELA, edit detection</small></a>
|
||||||
<h4>Operate</h4>
|
<h4>Operate</h4>
|
||||||
@@ -359,7 +371,8 @@ li{text-align:left;margin:.2rem 0}
|
|||||||
<a class="dl {{o('tools')}}" href=/tools>◈ FREE TOOLS <small>DNS, headers, JWT, hasher</small></a>
|
<a class="dl {{o('tools')}}" href=/tools>◈ FREE TOOLS <small>DNS, headers, JWT, hasher</small></a>
|
||||||
<h4>Account</h4>
|
<h4>Account</h4>
|
||||||
<a class="dl {{o('inbox')}}" href=/inbox>◈ INBOX <small>no-KYC messaging</small></a>
|
<a class="dl {{o('inbox')}}" href=/inbox>◈ INBOX <small>no-KYC messaging</small></a>
|
||||||
<a class="dl {{o('keys')}}" href=/keys>◈ API KEYS <small>metered access, balance</small></a>
|
<a class="dl {{o('signup')}}" href=/signup>◈ SIGN UP <small>username + password, 10 seconds, no KYC</small></a>
|
||||||
|
<a class="dl {{o('keys')}}" href=/keys>◈ API KEYS <small>account, balance, metered keys</small></a>
|
||||||
<a class="dl {{o('pass')}}" href=/pass>◈ PASS <small>$10/mo all-access</small></a>
|
<a class="dl {{o('pass')}}" href=/pass>◈ PASS <small>$10/mo all-access</small></a>
|
||||||
<a class="dl {{o('passport')}}" href=/passport>◈ AGENT PASSPORT <small>machine-readable badge</small></a>
|
<a class="dl {{o('passport')}}" href=/passport>◈ AGENT PASSPORT <small>machine-readable badge</small></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -572,14 +585,19 @@ def page(sec, body):
|
|||||||
u = con.execute("SELECT username FROM users WHERE id=?", (uid,)).fetchone()
|
u = con.execute("SELECT username FROM users WHERE id=?", (uid,)).fetchone()
|
||||||
bal = get_balance(uid)
|
bal = get_balance(uid)
|
||||||
pas = has_pass(uid)
|
pas = has_pass(uid)
|
||||||
acct = ('<a href=/keys style="color:var(--acc2);text-decoration:none;font-size:.72rem">' +
|
acct = ('<a class=achip href=/keys title="balance — click for API keys & top-up">'
|
||||||
("★ PASS · " if pas else "") + "$" + f"{bal/100:.2f}" + " · " + esc(u["username"]) + "</a>")
|
+ ("★ PASS · " if pas else "") + "◈ $" + f"{bal/100:.2f}" + " · " + esc(u["username"]) + "</a>"
|
||||||
|
+ ' <a class=abtn href=/logout title="log out">EXIT</a>')
|
||||||
except Exception:
|
except Exception:
|
||||||
acct = ""
|
acct = ""
|
||||||
|
if not uid or not acct:
|
||||||
|
nxt = ("?next=" + urllib.parse.quote(request.path)) if request.path not in ("/", "/inbox", "/signup", "/logout") else ""
|
||||||
|
acct = ('<a class=abtn href=/inbox' + nxt + ' title="log in — username + password, nothing else">LOG IN</a>'
|
||||||
|
' <a class="abtn solid" href=/signup' + nxt + ' title="create account — username + password, ~10 seconds, no KYC">SIGN UP</a>')
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
st = get_settings(uid)
|
st = get_settings(uid)
|
||||||
return render_template_string(BASE, body=Markup(body), bmac=BMAC, o=lambda s2: "on" if s2 == sec else "",
|
return render_template_string(BASE, body=Markup(body), bmac=BMAC, o=lambda s2: "on" if s2 == sec else "",
|
||||||
n1=n1, n2=n2, acct=acct,
|
n1=n1, n2=n2, acct=Markup(acct),
|
||||||
CFG_JS="window.DRB=" + json.dumps(st) + ";")
|
CFG_JS="window.DRB=" + json.dumps(st) + ";")
|
||||||
|
|
||||||
|
|
||||||
@@ -628,6 +646,7 @@ API_INDEX = {
|
|||||||
"service": "dark0rbits",
|
"service": "dark0rbits",
|
||||||
"description": "IP intel, card BIN validation, 30-min SMS rentals, residential proxy lab, steganography, trackable files, no-KYC messaging, utilities.",
|
"description": "IP intel, card BIN validation, 30-min SMS rentals, residential proxy lab, steganography, trackable files, no-KYC messaging, utilities.",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
|
{"method": "GET", "path": "/signup", "desc": "No-KYC signup page (humans): username + password, 4+ chars, ~10 seconds. Agents: POST /inbox form act=register&u=NAME&p=PASS -> session cookie dark0rbits_tok (30 days) + $1 free trial credit."},
|
||||||
{"method": "GET/POST", "path": "/api/settings", "desc": "Per-account UI tunables (bg, warp, parallax, density, speed, twinkle, hue, grid, scan, toast, type). Agents can theme their own client. GET returns current; POST form key=val applies (validated + clamped)."},
|
{"method": "GET/POST", "path": "/api/settings", "desc": "Per-account UI tunables (bg, warp, parallax, density, speed, twinkle, hue, grid, scan, toast, type). Agents can theme their own client. GET returns current; POST form key=val applies (validated + clamped)."},
|
||||||
{"method": "GET", "path": "/deaddrop", "desc": "Burn-after-read encrypted notes. POST /api/deaddrop/create (body, burn_after 1-10, ttl_hours 1-72, password optional) -> token. 5c, free with PASS."},
|
{"method": "GET", "path": "/deaddrop", "desc": "Burn-after-read encrypted notes. POST /api/deaddrop/create (body, burn_after 1-10, ttl_hours 1-72, password optional) -> token. 5c, free with PASS."},
|
||||||
{"method": "GET", "path": "/shot", "desc": "Page capture: POST /api/shot/create {url} then GET /api/shot/status/<id>. SSRF-guarded. 25c, free with PASS."},
|
{"method": "GET", "path": "/shot", "desc": "Page capture: POST /api/shot/create {url} then GET /api/shot/status/<id>. SSRF-guarded. 25c, free with PASS."},
|
||||||
@@ -681,7 +700,7 @@ INDEXNOW = "a8f3d4rk0rbits9e2b1c7x5k8m2v4q6t8"
|
|||||||
@app.route("/sitemap.xml")
|
@app.route("/sitemap.xml")
|
||||||
def sitemap():
|
def sitemap():
|
||||||
S = "https://dark0rbits.thetempleofdoom.com"
|
S = "https://dark0rbits.thetempleofdoom.com"
|
||||||
pages = ["", "ip", "card", "sms", "proxy", "steg", "track", "eh", "forensics", "canary", "deaddrop", "shot", "score", "mail", "inbox", "passport", "pass", "keys", "tools"]
|
pages = ["", "ip", "card", "sms", "proxy", "steg", "track", "eh", "forensics", "canary", "deaddrop", "shot", "score", "mail", "inbox", "passport", "pass", "keys", "maglab", "tools", "signup"]
|
||||||
xml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + "".join(f"<url><loc>{S}/{p}</loc><changefreq>weekly</changefreq></url>" for p in pages) + "</urlset>"
|
xml = '<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' + "".join(f"<url><loc>{S}/{p}</loc><changefreq>weekly</changefreq></url>" for p in pages) + "</urlset>"
|
||||||
return xml, 200, {"Content-Type": "application/xml"}
|
return xml, 200, {"Content-Type": "application/xml"}
|
||||||
|
|
||||||
@@ -694,7 +713,7 @@ def llms():
|
|||||||
def aiplugin():
|
def aiplugin():
|
||||||
return jsonify({"name_for_model": "dark0rbits", "schema_version": "v1",
|
return jsonify({"name_for_model": "dark0rbits", "schema_version": "v1",
|
||||||
"description_for_model": "IP intelligence, card BIN validation, SMS number rentals, proxy egress testing, LSB steganography, trackable file links with open-notifications, no-KYC site messaging.",
|
"description_for_model": "IP intelligence, card BIN validation, SMS number rentals, proxy egress testing, LSB steganography, trackable file links with open-notifications, no-KYC site messaging.",
|
||||||
"api": {"type": "openapi", "url": SITE + "/openapi.json"}, "auth": {"type": "none"}, "contact_email": "indianaholmes1@icloud.com"})
|
"api": {"type": "openapi", "url": SITE + "/openapi.json"}, "auth": {"type": "none"}, "contact_email": "makemoneys8@proton.me"})
|
||||||
|
|
||||||
@app.route("/openapi.json")
|
@app.route("/openapi.json")
|
||||||
def openapi():
|
def openapi():
|
||||||
@@ -727,6 +746,7 @@ def openapi():
|
|||||||
add("/api/shot/create", "post", "Queue page capture", {"url": "target url"}, req=True)
|
add("/api/shot/create", "post", "Queue page capture", {"url": "target url"}, req=True)
|
||||||
add("/api/shot/status/{id}", "get", "Shot result (png_b64 or text_fallback)")
|
add("/api/shot/status/{id}", "get", "Shot result (png_b64 or text_fallback)")
|
||||||
add("/api/score", "get", "Composite fraud score 0-100", {"ip": "opt", "email": "opt", "bin": "opt"})
|
add("/api/score", "get", "Composite fraud score 0-100", {"ip": "opt", "email": "opt", "bin": "opt"})
|
||||||
|
add("/signup", "get", "No-KYC signup page (username + password only)")
|
||||||
return jsonify(ps)
|
return jsonify(ps)
|
||||||
|
|
||||||
# ---------- 1. IP INTEL (auto + manual target) ----------
|
# ---------- 1. IP INTEL (auto + manual target) ----------
|
||||||
@@ -1371,7 +1391,7 @@ def steg_extract(img_bytes, password="", bits=None, spread=None):
|
|||||||
if bytes(raw[:4]) != b"AUR1":
|
if bytes(raw[:4]) != b"AUR1":
|
||||||
return None, f"no DARK0RBITS payload found with LSB depth {bits} (try other depth / randomized)"
|
return None, f"no DARK0RBITS payload found with LSB depth {bits} (try other depth / randomized)"
|
||||||
ln = struct.unpack(">I", bytes(raw[4:8]))[0]
|
ln = struct.unpack(">I", bytes(raw[4:8]))[0]
|
||||||
need = 64 + ln * 8
|
need = 96 + ln * 8 # header is 12 bytes (AUR1+len+phash) = 96 bits; old 64 truncated 4 bytes off every payload
|
||||||
data = bytes(raw)
|
data = bytes(raw)
|
||||||
if len(data) < 12: return None, "payload too small"
|
if len(data) < 12: return None, "payload too small"
|
||||||
if bytes(data[:4]) != b"AUR1":
|
if bytes(data[:4]) != b"AUR1":
|
||||||
@@ -1900,7 +1920,7 @@ def parse_headers(raw):
|
|||||||
env_from = out["return_path"].strip("<>")
|
env_from = out["return_path"].strip("<>")
|
||||||
frm = out["from"]
|
frm = out["from"]
|
||||||
m_from = re.search(r"<([^>]+)>", frm)
|
m_from = re.search(r"<([^>]+)>", frm)
|
||||||
addr_from = (m_from.group(1) if m_from else frm).split()[-1].strip("<>").lower()
|
addr_from = ((m_from.group(1) if m_from else frm).split() or [""])[-1].strip("<>").lower()
|
||||||
if env_from and addr_from and env_from.split("@")[-1] != addr_from.split("@")[-1]:
|
if env_from and addr_from and env_from.split("@")[-1] != addr_from.split("@")[-1]:
|
||||||
flags.append(f"envelope-from domain ({env_from.split('@')[-1]}) != From domain ({addr_from.split('@')[-1]}) — classic spoof marker")
|
flags.append(f"envelope-from domain ({env_from.split('@')[-1]}) != From domain ({addr_from.split('@')[-1]}) — classic spoof marker")
|
||||||
if out["reply_to"]:
|
if out["reply_to"]:
|
||||||
@@ -2187,7 +2207,9 @@ def inbox():
|
|||||||
tok = secrets.token_urlsafe(24)
|
tok = secrets.token_urlsafe(24)
|
||||||
con.execute("INSERT INTO sessions(token,user_id,created) VALUES(?,?,?)", (tok, con.execute("SELECT id FROM users WHERE username=?", (u,)).fetchone()["id"], int(time.time())))
|
con.execute("INSERT INTO sessions(token,user_id,created) VALUES(?,?,?)", (tok, con.execute("SELECT id FROM users WHERE username=?", (u,)).fetchone()["id"], int(time.time())))
|
||||||
con.commit()
|
con.commit()
|
||||||
resp = Response(status=302); resp.headers["Location"] = "/inbox"; resp.set_cookie("dark0rbits_tok", tok, max_age=86400*30, httponly=True)
|
nxt = request.form.get("next") or "/inbox"
|
||||||
|
if not nxt.startswith("/") or nxt.startswith("//"): nxt = "/inbox"
|
||||||
|
resp = Response(status=302); resp.headers["Location"] = nxt; resp.set_cookie("dark0rbits_tok", tok, max_age=86400*30, httponly=True)
|
||||||
return resp
|
return resp
|
||||||
elif action == "login":
|
elif action == "login":
|
||||||
u, p = (request.form.get("u") or "").strip()[:32], request.form.get("p") or ""
|
u, p = (request.form.get("u") or "").strip()[:32], request.form.get("p") or ""
|
||||||
@@ -2199,7 +2221,9 @@ def inbox():
|
|||||||
tok = secrets.token_urlsafe(24)
|
tok = secrets.token_urlsafe(24)
|
||||||
con.execute("INSERT INTO sessions(token,user_id,created) VALUES(?,?,?)", (tok, r["id"], int(time.time())))
|
con.execute("INSERT INTO sessions(token,user_id,created) VALUES(?,?,?)", (tok, r["id"], int(time.time())))
|
||||||
con.commit()
|
con.commit()
|
||||||
resp = Response(status=302); resp.headers["Location"] = "/inbox"; resp.set_cookie("dark0rbits_tok", tok, max_age=86400*30, httponly=True)
|
nxt = request.form.get("next") or "/inbox"
|
||||||
|
if not nxt.startswith("/") or nxt.startswith("//"): nxt = "/inbox"
|
||||||
|
resp = Response(status=302); resp.headers["Location"] = nxt; resp.set_cookie("dark0rbits_tok", tok, max_age=86400*30, httponly=True)
|
||||||
return resp
|
return resp
|
||||||
return page("inbox", "<h1>INBOX</h1><div class=card><span class=tag bad>bad login</span></div>")
|
return page("inbox", "<h1>INBOX</h1><div class=card><span class=tag bad>bad login</span></div>")
|
||||||
elif action == "logout":
|
elif action == "logout":
|
||||||
@@ -2233,6 +2257,42 @@ def inbox():
|
|||||||
<div class=card style=color:var(--dim)>API: (cookie auth) POST /inbox act=send body=… · GET /api/inbox/messages</div>"""
|
<div class=card style=color:var(--dim)>API: (cookie auth) POST /inbox act=send body=… · GET /api/inbox/messages</div>"""
|
||||||
return page("inbox", body)
|
return page("inbox", body)
|
||||||
|
|
||||||
|
@app.route("/signup", methods=["GET"])
|
||||||
|
def signup():
|
||||||
|
if current_user_id():
|
||||||
|
return Redirect("/keys")
|
||||||
|
nxt = esc(request.args.get("next") or "")
|
||||||
|
body = f"""
|
||||||
|
<h1>SIGN <span>UP</span></h1><p class=sub>Name + password. That's the whole form — no email, no phone, no KYC, nothing to verify. $1 free credit lands in your balance the moment you're in.</p>
|
||||||
|
<div class="grid2">
|
||||||
|
<div class="card glow"><b>Create account — 10 seconds</b>
|
||||||
|
<form method=post action=/inbox>
|
||||||
|
<input type=hidden name=act value=register>
|
||||||
|
<input type=hidden name=next value="{nxt}">
|
||||||
|
<label>username</label><input name=u placeholder="e.g. ghost-77" maxlength=32 autocomplete=off style=width:100% required>
|
||||||
|
<label>password (4+ chars)</label><input name=p type=password placeholder="4+ characters" style="width:100%;margin:.5rem 0" required>
|
||||||
|
<button class=big style=margin-top:.3rem>▸ CREATE ACCOUNT — NO KYC</button>
|
||||||
|
</form></div>
|
||||||
|
<div class="card"><b>What you get immediately</b>
|
||||||
|
<ul>
|
||||||
|
<li>◈ <b>$1 free trial credit</b> — metered API calls work instantly</li>
|
||||||
|
<li>▣ <b>API keys</b> — machine access on the same balance</li>
|
||||||
|
<li>✉ <b>Inbox</b> — no-KYC messaging with the operator + file-open alerts</li>
|
||||||
|
<li>⚙ <b>Tunables</b> — your theme follows your account</li>
|
||||||
|
</ul>
|
||||||
|
<div style=color:var(--dim);font-size:.85rem;margin-top:.6rem>Already have an account? <a href=/inbox>Log in →</a></div>
|
||||||
|
</div>
|
||||||
|
</div>"""
|
||||||
|
return page("signup", body)
|
||||||
|
|
||||||
|
@app.route("/logout", methods=["GET"])
|
||||||
|
def logout():
|
||||||
|
con = db()
|
||||||
|
con.execute("DELETE FROM sessions WHERE token=?", (request.cookies.get("dark0rbits_tok"),)); con.commit()
|
||||||
|
resp = Response(status=302); resp.headers["Location"] = "/"
|
||||||
|
resp.set_cookie("dark0rbits_tok", "", max_age=0)
|
||||||
|
return resp
|
||||||
|
|
||||||
@app.route("/api/inbox/messages", methods=["GET"])
|
@app.route("/api/inbox/messages", methods=["GET"])
|
||||||
def api_inbox_msgs():
|
def api_inbox_msgs():
|
||||||
uid = current_user_id()
|
uid = current_user_id()
|
||||||
@@ -2265,6 +2325,27 @@ DD_EXPLAINER = """<div class=card><b>OPSEC NOTES</b><br><span style="color:var(-
|
|||||||
• Optional <span title="scrypt-hashed gate: a wrong or missing password shows the unlock form, never the note, and burns no reads">password gate</span> — wrong attempts cost nothing.
|
• Optional <span title="scrypt-hashed gate: a wrong or missing password shows the unlock form, never the note, and burns no reads">password gate</span> — wrong attempts cost nothing.
|
||||||
• Billing: free with <span title="PASS = $10/mo all-access subscription">PASS</span>, otherwise 5¢ per note from your metered balance.</span></div>"""
|
• Billing: free with <span title="PASS = $10/mo all-access subscription">PASS</span>, otherwise 5¢ per note from your metered balance.</span></div>"""
|
||||||
|
|
||||||
|
@app.route("/dead-drop")
|
||||||
|
def deaddrop_alias():
|
||||||
|
from flask import redirect
|
||||||
|
return redirect("/deaddrop", 301)
|
||||||
|
|
||||||
|
@app.route("/burner-mail")
|
||||||
|
def burnermail_alias():
|
||||||
|
from flask import redirect
|
||||||
|
return redirect("/mail", 301)
|
||||||
|
|
||||||
|
@app.route("/fraud-score")
|
||||||
|
def fraudscore_alias():
|
||||||
|
from flask import redirect
|
||||||
|
return redirect("/score", 301)
|
||||||
|
|
||||||
|
@app.route("/mag-lab")
|
||||||
|
def maglab_alias():
|
||||||
|
from flask import redirect
|
||||||
|
return redirect("/maglab", 301)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/deaddrop")
|
@app.route("/deaddrop")
|
||||||
def deaddrop():
|
def deaddrop():
|
||||||
uid = current_user_id()
|
uid = current_user_id()
|
||||||
@@ -2622,7 +2703,7 @@ def index():
|
|||||||
f'<p>{desc}</p><a class=tgo href=/{href}><button>Open →</button></a></div>'
|
f'<p>{desc}</p><a class=tgo href=/{href}><button>Open →</button></a></div>'
|
||||||
for href, name, desc, ico, cat in tools)
|
for href, name, desc, ico, cat in tools)
|
||||||
stat = f"you're connecting from <b style='color:var(--acc2)'>{esc(d.get('query','?'))}</b> · {esc(d.get('country',''))}"
|
stat = f"you're connecting from <b style='color:var(--acc2)'>{esc(d.get('query','?'))}</b> · {esc(d.get('country',''))}"
|
||||||
cta = ('<a href=/inbox><button class=big>◈ INBOX</button></a> <a href=/keys><button class="big ghost">▣ API KEYS</button></a> <a href=/pass><button class=big>★ GET PASS</button></a>' if uid else '<a href=/inbox><button class=big>▸ SIGN UP — NO KYC</button></a> <a href=/inbox><button class="big ghost">◈ LOG IN</button></a> <a href=/pass><button class="big ghost">★ GET PASS</button></a>')
|
cta = ('<a href=/inbox><button class=big>◈ INBOX</button></a> <a href=/keys><button class="big ghost">▣ API KEYS</button></a> <a href=/pass><button class=big>★ GET PASS</button></a>' if uid else '<a href=/signup><button class=big>▸ SIGN UP — NO KYC</button></a> <a href=/inbox><button class="big ghost">◈ LOG IN</button></a> <a href=/pass><button class="big ghost">★ GET PASS</button></a>')
|
||||||
from markupsafe import escape as _e
|
from markupsafe import escape as _e
|
||||||
stat_line = '<span class="hl">▸ ' + stat + '</span>' if stat else ""
|
stat_line = '<span class="hl">▸ ' + stat + '</span>' if stat else ""
|
||||||
body = f"""
|
body = f"""
|
||||||
@@ -2709,5 +2790,276 @@ def not_found(e):
|
|||||||
return page("home", body), 404
|
return page("home", body), 404
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- MAG-LAB (browser magstripe studio, closed-loop only) ----------
|
||||||
|
_MAG_POLICY = (
|
||||||
|
"MAG-LAB encodes CLOSED-LOOP cards only: your own gift, loyalty, membership, "
|
||||||
|
"event or staff cards. Payment-network tracks (bank/debit/credit layouts, "
|
||||||
|
"13-19 digit Luhn-valid PANs, bank service codes 101/121/201-220, EMV/JCOP "
|
||||||
|
"dumps) are refused at encode AND decode. This is a hard policy, not a "
|
||||||
|
"toggle you can switch off.")
|
||||||
|
|
||||||
|
def _mag_sentinel(track):
|
||||||
|
"""True if a track looks like a payment-network card rather than closed-loop."""
|
||||||
|
import re as _re
|
||||||
|
t = (track or "").strip()
|
||||||
|
if not t:
|
||||||
|
return False
|
||||||
|
body = t[1:] if t[0] in "%;" else t
|
||||||
|
pan = _re.sub(r"[^0-9]", "", body.split("^")[0] if "^" in body else (body.split("=")[0] if "=" in body else body))
|
||||||
|
if pan and 13 <= len(pan) <= 19:
|
||||||
|
s, alt = 0, False
|
||||||
|
for ch in reversed(pan):
|
||||||
|
d = ord(ch) - 48
|
||||||
|
if alt:
|
||||||
|
d *= 2
|
||||||
|
if d > 9:
|
||||||
|
d -= 9
|
||||||
|
s += d
|
||||||
|
alt = not alt
|
||||||
|
if s % 10 == 0:
|
||||||
|
return True
|
||||||
|
m = _re.search(r"\^([0-9]{4})([0-9]{3})", t)
|
||||||
|
if m and m.group(2)[0] in ("1", "2"):
|
||||||
|
return True
|
||||||
|
m = _re.search(r"=([0-9]{4})([0-9]{3})", t)
|
||||||
|
if m and m.group(2)[0] in ("1", "2"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
_MAGLAB_HTML = r"""<!DOCTYPE html>
|
||||||
|
<h1>MAG <span>LAB</span></h1>
|
||||||
|
<p class=sub>Browser magstripe studio — Chrome + Web Serial talks straight to your MSR605/606-class writer. No drivers, no desktop app, any OS. Encode, read, decode, batch-issue <b>closed-loop cards</b>: gift, loyalty, membership, event tickets, staff badges.</p>
|
||||||
|
<div class="card" style="border-color:var(--bad)">
|
||||||
|
<b>POLICY</b><br><span style="color:var(--dim)">__POLICY__</span>
|
||||||
|
</div>
|
||||||
|
<div class=grid2>
|
||||||
|
<div class=card>
|
||||||
|
<b>CONNECT</b>
|
||||||
|
<div style=margin:.6rem 0><button id=magconn>plug in writer</button> <span id=magstat style=color:var(--dim)>not connected</span></div>
|
||||||
|
<div style=color:var(--dim);font-size:.8rem>Chrome/Edge/Opera on Windows, macOS, Linux or ChromeOS. Firefox/Safari do not ship Web Serial. Writer must be an MSR605/606 or compatible serial MagStripe encoder.</div>
|
||||||
|
</div>
|
||||||
|
<div class=card>
|
||||||
|
<b>ISSUE A CARD</b>
|
||||||
|
<form id=magform style=margin:.6rem 0>
|
||||||
|
<div style=margin:.3rem 0><label>schema</label>
|
||||||
|
<select id=magschema>
|
||||||
|
<option value="gift">GIFT — serial + pin (track 2)</option>
|
||||||
|
<option value="loyal">LOYALTY — member# + tier (track 2)</option>
|
||||||
|
<option value="event">EVENT — event# + seat (t1+t2)</option>
|
||||||
|
<option value="staff">STAFF — employee# + dept (t1+t2)</option>
|
||||||
|
<option value="custom">CUSTOM — raw track editor</option>
|
||||||
|
</select></div>
|
||||||
|
<div id=magfields style=margin:.4rem 0></div>
|
||||||
|
<button>encode → writer</button> <button type=button id=magread>read card → decode</button>
|
||||||
|
</form>
|
||||||
|
<div id=magout style="margin-top:.6rem;font-size:.85rem;color:var(--dim)"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class=card>
|
||||||
|
<b>BATCH ISSUE</b> <span style=color:var(--dim)>(20c/card — paste CSV lines: label,value)</span>
|
||||||
|
<textarea id=magcsv rows=4 style="width:100%;margin:.4rem 0" placeholder="GIFT001,1000 GIFT002,2500 STAFF-JANE,EMP1042"></textarea>
|
||||||
|
<button type=button id=magbatprep>prepare batch</button> <span id=magbatstat style=color:var(--dim)></span>
|
||||||
|
<div id=magbar style="display:none;margin:.5rem 0;height:14px;border:1px solid var(--line);border-radius:8px;overflow:hidden"><div id=magbarfill style="height:100%;width:0;background:linear-gradient(90deg,var(--acc),var(--acc2));transition:width .3s"></div></div>
|
||||||
|
<button type=button id=magprint style=margin-top:.5rem>🖨 print card faces</button>
|
||||||
|
</div>
|
||||||
|
<div class=card>
|
||||||
|
<b>TRACK VISUALIZER</b>
|
||||||
|
<div style=color:var(--dim);font-size:.8rem;margin:.3rem 0>paste any track to see its structure decoded (read-only field map; payment shapes are masked)</div>
|
||||||
|
<input id=magviz placeholder=";ABC=1234?" style=width:100%>
|
||||||
|
<pre id=magvizout style="margin:.5rem 0;color:var(--acc2);font-size:.75rem;white-space:pre-wrap"></pre>
|
||||||
|
</div>
|
||||||
|
<div class=card><b>API</b> <span style=color:var(--dim)>(agent-first — 30c/encode, 20c/card batch, via key)</span>
|
||||||
|
__AGENT__
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
var SCHEMAS = {
|
||||||
|
gift: {fields:[["serial","serial number"],["pin","4-7 digit pin"]], make:function(f){
|
||||||
|
var pin=(f.pin||"").replace(/[^0-9]/g,""); if(pin.length<4) return {err:"pin needs 4+ digits"};
|
||||||
|
var ser=(f.serial||"").replace(/[^A-Z0-9]/gi,"").toUpperCase(); if(!ser) return {err:"serial required"};
|
||||||
|
var t2=";"+ser+"="+pin+"?"; return {t2:t2,label:"GIFT "+ser};}},
|
||||||
|
loyal: {fields:[["member","member number"],["tier","tier letter (A-F)"]], make:function(f){
|
||||||
|
var m=(f.member||"").replace(/[^0-9]/g,""); if(!m) return {err:"member # required"};
|
||||||
|
var tier=(f.tier||"A").toUpperCase().replace(/[^A-F]/g,"")||"A";
|
||||||
|
var t2=";"+m+"="+tier+Date.now().toString(36).slice(-4)+"?"; return {t2:t2,label:"LOYAL "+m};}},
|
||||||
|
event: {fields:[["event","event id"],["seat","seat"]], make:function(f){
|
||||||
|
var e=(f.event||"").replace(/[^A-Z0-9]/gi,"").toUpperCase(), s=(f.seat||"").replace(/[^A-Z0-9]/gi,"").toUpperCase();
|
||||||
|
if(!e||!s) return {err:"event + seat required"};
|
||||||
|
var t1="%"+e+"^"+s+"^"+new Date().toISOString().slice(2,10).replace(/-/g,"");
|
||||||
|
var t2=";"+e+"="+s+"?"; return {t1:t1,t2:t2,label:"EVT "+e};}},
|
||||||
|
staff: {fields:[["employee","employee #"],["dept","department"]], make:function(f){
|
||||||
|
var e=(f.employee||"").replace(/[^0-9]/g,""), d=(f.dept||"").replace(/[^A-Z0-9]/gi,"").toUpperCase();
|
||||||
|
if(!e) return {err:"employee # required"};
|
||||||
|
var t1="%"+d+"^EMP"+e+"^"+new Date().toISOString().slice(2,10).replace(/-/g,"");
|
||||||
|
var t2=";"+e+"="+d+"?"; return {t1:t1,t2:t2,label:"STAFF "+e};}},
|
||||||
|
custom:{fields:[["t1","track 1 (raw)"],["t2","track 2 (raw)"]], make:function(f){
|
||||||
|
return {t1:f.t1||"",t2:f.t2||"",label:"CUSTOM"};}}
|
||||||
|
};
|
||||||
|
var port=null, writer=null;
|
||||||
|
var BATCH=null, BATIDX=0;
|
||||||
|
function magLog(m){document.getElementById("magout").textContent=m}
|
||||||
|
document.getElementById("magschema").addEventListener("change",function(){
|
||||||
|
var s=SCHEMAS[this.value], h="";
|
||||||
|
s.fields.forEach(function(f){h+='<input id="mf_'+f[0]+'" placeholder="'+f[1]+'" style="width:100%;margin:.25rem 0">'});
|
||||||
|
document.getElementById("magfields").innerHTML=h;
|
||||||
|
});
|
||||||
|
document.getElementById("magschema").dispatchEvent(new Event("change"));
|
||||||
|
document.getElementById("magconn").addEventListener("click",async function(){
|
||||||
|
try{
|
||||||
|
port=await navigator.serial.requestPort();
|
||||||
|
await port.open({baudRate:9600});
|
||||||
|
writer=port.writable.getWriter();
|
||||||
|
document.getElementById("magstat").textContent="connected ✓";
|
||||||
|
}catch(e){document.getElementById("magstat").textContent="error: "+e.message}
|
||||||
|
});
|
||||||
|
async function encodeCheck(t1,t2){
|
||||||
|
var r=await fetch("/api/maglab/encode",{method:"POST",headers:{"Content-Type":"application/json","X-Requested-With":"maglab"},body:JSON.stringify({t1:t1||"",t2:t2||""})});
|
||||||
|
return await r.json();
|
||||||
|
}
|
||||||
|
document.getElementById("magform").addEventListener("submit",async function(ev){
|
||||||
|
ev.preventDefault();
|
||||||
|
if(!writer){magLog("connect the writer first");return}
|
||||||
|
if(BATCH && BATIDX<BATCH.length){
|
||||||
|
var c=BATCH[BATIDX];
|
||||||
|
magLog("writing "+(BATIDX+1)+"/"+BATCH.length+": "+c.label+" — insert blank card…");
|
||||||
|
var cmd="\x1b[2S;"+c.t2.slice(1)+"\x04";
|
||||||
|
await writer.write(new TextEncoder().encode(cmd));
|
||||||
|
BATIDX++;
|
||||||
|
document.getElementById("magbarfill").style.width=Math.round(100*BATIDX/BATCH.length)+"%";
|
||||||
|
magLog("✓ "+BATIDX+"/"+BATCH.length+" written."+(BATIDX<BATCH.length?" next: "+BATCH[BATIDX].label:" BATCH COMPLETE ✓ — hit 🖨 print card faces"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var sc=SCHEMAS[document.getElementById("magschema").value], f={};
|
||||||
|
sc.fields.forEach(function(x){f[x[0]]=document.getElementById("mf_"+x[0]).value});
|
||||||
|
var built=sc.make(f);
|
||||||
|
if(built.err){magLog("✗ "+built.err);return}
|
||||||
|
var j=await encodeCheck(built.t1,built.t2);
|
||||||
|
if(!j.ok){magLog("✗ refused: "+(j.error||"unknown"));return}
|
||||||
|
var cmds=[];
|
||||||
|
if(built.t1)cmds.push("\x1b[1S;"+built.t1.slice(1)+"\x04");
|
||||||
|
if(built.t2)cmds.push("\x1b[2S;"+built.t2.slice(1)+"\x04");
|
||||||
|
for(const c of cmds){await writer.write(new TextEncoder().encode(c))}
|
||||||
|
magLog("✓ encoded to card: "+built.label+"\n"+[built.t1,built.t2].filter(Boolean).join("\n"));
|
||||||
|
});
|
||||||
|
document.getElementById("magbatprep").addEventListener("click",async function(){
|
||||||
|
var lines=document.getElementById("magcsv").value.split("\n").map(function(x){return x.trim()}).filter(Boolean)
|
||||||
|
.map(function(x){var p=x.split(",");return {label:(p[0]||"").trim(),value:(p[1]||"").trim()}});
|
||||||
|
if(!lines.length){document.getElementById("magbatstat").textContent="paste CSV first";return}
|
||||||
|
var r=await fetch("/api/maglab/batch",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({rows:lines})});
|
||||||
|
var j=await r.json();
|
||||||
|
if(!j.ok){document.getElementById("magbatstat").textContent="✗ "+(j.error||"refused");return}
|
||||||
|
BATCH=j.cards; BATIDX=0;
|
||||||
|
document.getElementById("magbar").style.display="block"; document.getElementById("magbarfill").style.width="0";
|
||||||
|
document.getElementById("magbatstat").textContent="ready: "+j.count+" cards · "+(j.cost_cents?("$"+(j.cost_cents/100).toFixed(2)+" charged"):"PASS");
|
||||||
|
});
|
||||||
|
document.getElementById("magprint").addEventListener("click",function(){
|
||||||
|
if(!BATCH){magLog("prepare a batch first");return}
|
||||||
|
var w=window.open("","_blank");
|
||||||
|
var cards=BATCH.map(function(c){
|
||||||
|
var qr="https://api.qrserver.com/v1/create-qr-code/?size=110x110&data="+encodeURIComponent(c.qr);
|
||||||
|
return '<div class="cf"><div class="lbl">'+c.label+'</div><img src="'+qr+'" width="110" height="110"><div class="pin">PIN '+c.pin+'</div><div class="mk">dark0rbits · mag-lab · scan to activate</div></div>';
|
||||||
|
}).join("");
|
||||||
|
w.document.write('<html><head><title>card faces</title><style>body{font-family:-apple-system,sans-serif;background:#fff;color:#111}.cf{display:inline-block;width:3.37in;height:2.125in;border:1px dashed #999;border-radius:12px;margin:8px;padding:10px;text-align:center;vertical-align:top;page-break-inside:avoid}.lbl{font-weight:800;font-size:15px}.pin{font-size:12px;margin-top:4px;color:#333}.mk{font-size:9px;color:#888;margin-top:6px}.nopr{margin-bottom:10px}@media print{.nopr{display:none}}</style></head><body>');
|
||||||
|
w.document.write('<div class=nopr><button onclick="print()">🖨 print</button> each QR is the card\'s digital twin — balance lookup by label+PIN</div>');
|
||||||
|
w.document.write(cards+"</body></html>");
|
||||||
|
w.document.close();
|
||||||
|
});
|
||||||
|
document.getElementById("magread").addEventListener("click",async function(){
|
||||||
|
if(!port){magLog("connect the writer first");return}
|
||||||
|
magLog("swipe a card, then paste the raw track into the visualizer below to decode");
|
||||||
|
});
|
||||||
|
document.getElementById("magviz").addEventListener("input",async function(){
|
||||||
|
var v=this.value;
|
||||||
|
if(v.length<4){return}
|
||||||
|
var r=await fetch("/api/maglab/decode",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({track:v})});
|
||||||
|
var j=await r.json();
|
||||||
|
document.getElementById("magvizout").textContent=JSON.stringify(j,null,1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@app.route("/maglab")
|
||||||
|
def maglab():
|
||||||
|
body = _MAGLAB_HTML.replace("__POLICY__", _MAG_POLICY)
|
||||||
|
body = body.replace("__AGENT__", agent_card(
|
||||||
|
"POST /api/maglab/encode {t1,t2} · POST /api/maglab/batch {rows:[{label,value}]}",
|
||||||
|
"curl -X POST " + SITE + "/api/maglab/encode -H 'Authorization: Bearer KEY' -d '{\"t2\":\";GIFT000123=4321?\"}'",
|
||||||
|
"Closed-loop magstripe studio. Encode validates + LRC-checks (30c), batch issues up to 100 cards with PINs + QR twins (20c/card). Refuses payment-card shapes."))
|
||||||
|
body += gloss([("ISO 7811", "the magstripe track format standard - track1 79-bit alnum, track2/3 5-bit numeric"),
|
||||||
|
("LRC", "longitudinal redundancy check - the trailing ? sentinel; wrong LRC = unreadable card"),
|
||||||
|
("closed-loop", "a card scheme you own end-to-end: your shop issues it, your shop redeems it")])
|
||||||
|
return page("maglab", body)
|
||||||
|
|
||||||
|
@app.route("/api/maglab/encode", methods=["POST"])
|
||||||
|
def api_maglab_encode():
|
||||||
|
r = rate_limit("maglab", 20, 60)
|
||||||
|
if r: return r
|
||||||
|
uid = key_user() or current_user_id()
|
||||||
|
if not uid:
|
||||||
|
return jsonify({"ok": False, "error": "auth required: account session (sign up at /inbox, no KYC) or Authorization: Bearer *** key"}), 401
|
||||||
|
if not has_pass(uid) and not charge(uid, 30, "maglab encode"):
|
||||||
|
return jsonify({"ok": False, "error": "insufficient balance", "balance_cents": get_balance(uid), "topup": SITE + "/keys"}), 402
|
||||||
|
t1 = str(jp("t1") or "").strip()
|
||||||
|
t2 = str(jp("t2") or "").strip()
|
||||||
|
if not t1 and not t2:
|
||||||
|
return jsonify({"ok": False, "error": "at least one track required"}), 400
|
||||||
|
for t in (t1, t2):
|
||||||
|
if t and _mag_sentinel(t):
|
||||||
|
return jsonify({"ok": False, "error": "refused: payment-network card shape detected - " + _MAG_POLICY[:120]}), 403
|
||||||
|
for name, t in (("t1", t1), ("t2", t2)):
|
||||||
|
if t and not t.endswith("?"):
|
||||||
|
return jsonify({"ok": False, "error": name + " missing terminator '?' (LRC sentinel)"}), 400
|
||||||
|
return jsonify({"ok": True, "ready": True, "t1": t1, "t2": t2})
|
||||||
|
|
||||||
|
@app.route("/api/maglab/batch", methods=["POST"])
|
||||||
|
def api_maglab_batch():
|
||||||
|
r = rate_limit("maglab_batch", 6, 60)
|
||||||
|
if r: return r
|
||||||
|
uid = key_user() or current_user_id()
|
||||||
|
if not uid:
|
||||||
|
return jsonify({"ok": False, "error": "auth required: account session (sign up at /inbox, no KYC) or Authorization: Bearer *** key"}), 401
|
||||||
|
rows = jp("rows")
|
||||||
|
if not isinstance(rows, list) or not rows:
|
||||||
|
return jsonify({"ok": False, "error": "rows: array of {label,value} required"}), 400
|
||||||
|
if len(rows) > 100:
|
||||||
|
return jsonify({"ok": False, "error": "max 100 cards per batch"}), 400
|
||||||
|
pas = has_pass(uid)
|
||||||
|
if not pas and not charge(uid, 20 * len(rows), "maglab batch x%d" % len(rows)):
|
||||||
|
return jsonify({"ok": False, "error": "insufficient balance", "balance_cents": get_balance(uid), "topup": SITE + "/keys"}), 402
|
||||||
|
made = []
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
label = str(row.get("label") or ("CARD%03d" % (i + 1)))[:40]
|
||||||
|
value = str(row.get("value") or "").strip()
|
||||||
|
if not value:
|
||||||
|
return jsonify({"ok": False, "error": "row %d: value required" % (i + 1)}), 400
|
||||||
|
pin = "".join(str(random.randrange(10)) for _ in range(6))
|
||||||
|
t2 = ";" + re.sub(r"[^A-Z0-9]", "", value.upper()) + "=" + pin + "?"
|
||||||
|
if _mag_sentinel(t2):
|
||||||
|
return jsonify({"ok": False, "error": "row %d: refused, payment-card shape" % (i + 1)}), 403
|
||||||
|
made.append({"label": label, "t2": t2, "pin": pin, "qr": SITE + "/card?card=" + label + ":" + pin})
|
||||||
|
return jsonify({"ok": True, "count": len(made), "cards": made,
|
||||||
|
"cost_cents": 0 if pas else 20 * len(rows)})
|
||||||
|
|
||||||
|
@app.route("/api/maglab/decode", methods=["POST"])
|
||||||
|
def api_maglab_decode():
|
||||||
|
r = rate_limit("maglab_dec", 40, 60)
|
||||||
|
if r: return r
|
||||||
|
t = str(jp("track") or "").strip()
|
||||||
|
if not t:
|
||||||
|
return jsonify({"ok": False, "error": "track required"}), 400
|
||||||
|
if _mag_sentinel(t):
|
||||||
|
return jsonify({"ok": False, "masked": True, "error": "payment-card shape - fields masked by policy"}), 200
|
||||||
|
t1 = t.startswith("%")
|
||||||
|
sep = "^" if t1 else "="
|
||||||
|
body = t[1:] if t[0] in "%;" else t
|
||||||
|
fields = body.rstrip("?").split(sep)
|
||||||
|
return jsonify({"ok": True, "track": 1 if t1 else 2,
|
||||||
|
"format": "ISO7811-A" if t1 else "ISO7811-B",
|
||||||
|
"fields": [f[:40] for f in fields],
|
||||||
|
"lrc_sentinel": body.endswith("?"),
|
||||||
|
"note": "closed-loop decode"})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, threaded=True)
|
app.run(host="0.0.0.0", port=5000, threaded=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user