From 79470e471c9c6e06b257065c2a441da2f4a7d651 Mon Sep 17 00:00:00 2001 From: martbost Date: Wed, 2 Sep 2026 06:46:06 -0500 Subject: [PATCH] Members area: weekly webinar replay page at /replays Wallet-gated like the Method lessons: the API returns titles/dates to everyone (prospects see the shape of the product) and video URLs only with a member session. List lives in the data volume (replays.json) so the weekly add never needs a deploy; videos stream from the DO Spaces bucket (CSP media-src added). Teaser on /training; chatbot + AI prompt updated. Co-Authored-By: Claude Fable 5 --- public/chat.js | 2 +- public/replays.html | 17 +++++++++++ public/replays.js | 71 ++++++++++++++++++++++++++++++++++++++++++++ public/training.html | 2 ++ server.js | 17 +++++++++-- 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 public/replays.html create mode 100644 public/replays.js diff --git a/public/chat.js b/public/chat.js index 535e20a..1d62181 100644 --- a/public/chat.js +++ b/public/chat.js @@ -48,7 +48,7 @@ {k:['cap','capped','cap out','max level','maximum level','8 levels','eight levels','after corona','top level','stop earning','earning cap','level cap','beyond corona'], a:()=>`Your level caps at Corona (level 8) — you can't upgrade higher. But your earning doesn't cap. Two reasons: (1) you catch an upgrade pass-up from every person in your first seven generations as they level up, and those generations hold up to 254 positions that keep filling over time. (2) Pass-ups travel UP past anyone who can't catch them — when someone upgrades, the payment rises to the first upline who's qualified and leveled high enough (up to the contract's 16-layer depth cap; otherwise it goes to the project fee wallet). So Corona is the position that misses nothing: at max level you're eligible for every pass-up, you're never the bottleneck, and you catch the ones under-leveled members below you let slip. It's the whole reason to keep leveling up. Full breakdown: rmcircle.team/how-pay-works#cap — contract mechanics, verifiable on-chain, never a promise of income.`}, {k:['members area','member area','whats the product','what is the product','what am i buying','what do i get','members only','locked','unlock','sign in to training'], - a:()=>`Yes — every position includes a real members area: The Circle Method (10-lesson recruiting & coaching course + a real cash-out walkthrough) on the training page, your live coaching dashboard, promo tools personalized with your invite link, printable playbooks, wallet-verified team messaging, and the Telegram companion bot + Mini App. No passwords — access is proven by the wallet that owns your position (one free signature that can't move funds), and inside the Telegram Mini App it unlocks automatically. Lesson 1 and all the how-it-works videos are public so you can inspect everything before joining.`}, + a:()=>`Yes — every position includes a real members area: The Circle Method (10-lesson recruiting & coaching course + a real cash-out walkthrough) on the training page, the weekly team webinar replays, your live coaching dashboard, promo tools personalized with your invite link, printable playbooks, wallet-verified team messaging, and the Telegram companion bot + Mini App. No passwords — access is proven by the wallet that owns your position (one free signature that can't move funds), and inside the Telegram Mini App it unlocks automatically. Lesson 1 and all the how-it-works videos are public so you can inspect everything before joining.`}, {k:['no money','cant afford','can not afford',"can't afford",'broke','they dont have','waiting on payday','no funds to join'], a:()=>`"No money" is usually one of three things — sort it first. (1) Most often it means "this sounds expensive/complicated": entry is ${pol()} POL${usd()||" — a small one-time amount"} — payable with a regular debit card right on the join page. (2) Sometimes it's a priority thing: send the "Pocket Change" video from Promo Tools and let it do the talking. (3) If they're genuinely broke: tell them honestly "don't join yet" — never rent money, never money they can't afford to lose. Ask "when's payday?" and follow up then — their invite link doesn't expire and the team keeps building publicly while they wait. One hard rule: never pay someone's entry for them.`}, {k:['own the level','need to own','have to buy the level','catch without','still at scintilla','upline not upgraded','sponsor not upgraded','only pays 2','only pays two','only 2 payments','seems small','2 then 4','level is your reach'], diff --git a/public/replays.html b/public/replays.html new file mode 100644 index 0000000..a253052 --- /dev/null +++ b/public/replays.html @@ -0,0 +1,17 @@ +Webinar Replays | RM Circle Team Build + + + + + +
+
Members area

The team meets every week.

Live team webinars: what's working, member questions answered straight, and the plays to run this week. Miss one and the replay is here — included with every position, like the rest of the members area.

+ +
+
+ +
Risk reminder: webinars discuss a cryptocurrency team build. Nothing in them is an income promise or financial advice. Participation involves real risk — use only funds you can afford to lose.
+
+
+ + diff --git a/public/replays.js b/public/replays.js new file mode 100644 index 0000000..3a6bc0b --- /dev/null +++ b/public/replays.js @@ -0,0 +1,71 @@ +// Webinar replays — members-area page. The list comes from /api/public/replays: +// locked responses carry titles/dates only (prospects see the shape of the +// product), a member session gets the video URLs. Unlock is the same +// one-signature SIWE flow as the training page (msg-challenge/msg-verify via +// RMCWallet); Mini App visitors already carry a session and see no lock. +(function () { + 'use strict'; + function $(id) { return document.getElementById(id); } + function esc(s) { return String(s == null ? '' : s).replace(/[&<>'"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[c]; }); } + + function card(r, locked) { + var head = '
' + esc(r.date || '') + (r.minutes ? ' · ' + esc(r.minutes) + ' min' : '') + '
' + + '

' + esc(r.title || 'Team webinar') + '

' + + (r.blurb ? '

' + esc(r.blurb) + '

' : '') + '
'; + if (locked || !r.url) { + return '
' + head + + '
' + + '

🔒 Replays come with every team position. Sign in with the wallet that owns your position — one free signature, it can\'t move funds. Linked on Telegram? The ☰ Mini App in @RMCircleBot unlocks everything automatically.

' + + ' ' + + 'Not a member? See how to join' + + '
'; + } + return '
' + head + + '
'; + } + + var UNLOCK_LABEL = '🔓 I\'m a member — connect & unlock'; + function wireUnlock() { + document.querySelectorAll('button.rmc-unlock').forEach(function (b) { + b.addEventListener('click', function (ev) { + var btn = ev.currentTarget, err = btn.closest('.rmc-lock').querySelector('.rmc-lock-err'); + btn.disabled = true; btn.textContent = 'Connecting…'; + (async function () { + try { + var eth = window.RMCWallet && await window.RMCWallet.pick(); + if (!eth) { err.textContent = 'No wallet in this browser. On a phone, open this page inside your wallet app\'s browser — or use the Telegram Mini App.'; btn.disabled = false; btn.textContent = UNLOCK_LABEL; return; } + var accounts = await eth.request({ method: 'eth_requestAccounts' }); + var account = accounts && accounts[0]; + try { await window.RMCWallet.ensureChain(eth); } catch (ce) { err.textContent = 'Switch your wallet to the Polygon network, then tap unlock again.'; btn.disabled = false; btn.textContent = UNLOCK_LABEL; return; } + var ch = await (await fetch('/api/public/msg-challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account }) })).json(); + if (ch.error) throw new Error(ch.error); + var sig = await eth.request({ method: 'personal_sign', params: [ch.message, account] }); + var vr = await (await fetch('/api/public/msg-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account, signature: sig }) })).json(); + if (vr.error) throw new Error(vr.error); + load(); + } catch (e) { err.textContent = e.message || String(e); btn.disabled = false; btn.textContent = UNLOCK_LABEL; } + })(); + }); + }); + } + + function load() { + fetch('/api/public/replays').then(function (r) { return r.json(); }).then(function (d) { + var list = d.replays || []; + if (!list.length) { + $('replayList').innerHTML = '
The first replay is on its way — check back after this week\'s webinar.
'; + return; + } + if (!d.locked) { + $('replayStatus').innerHTML = '

✅ Members area unlocked — position #' + esc(d.id) + '

'; + } else { + $('replayStatus').innerHTML = ''; + } + $('replayList').innerHTML = list.map(function (r) { return card(r, d.locked); }).join(''); + if (d.locked) wireUnlock(); + }).catch(function () { + $('replayList').innerHTML = '
Could not load the replay list — refresh to try again.
'; + }); + } + load(); +})(); diff --git a/public/training.html b/public/training.html index a9200e1..473bdb1 100644 --- a/public/training.html +++ b/public/training.html @@ -29,6 +29,8 @@
Virtual card added to a mobile wallet, ready to tap to pay

The end result from the video: a virtual card added to a mobile wallet, ready to tap-to-pay — groceries, gas, anywhere cards are accepted. It started as POL catches from the team build.

+
🎥 The team meets every week. Live webinars with what's working right now and member questions answered straight — and every replay is waiting in the members area. Watch the webinar replays →
+

⭕ The Circle Method — Module 1: Get Your Two

Our recruiting course, taught by the team. Four short lessons that take you from "I could never recruit" to your first two curious people — the honest way: no pitching, no pressure, no income promises. Watch them in order, then run the plays. When your two join, send them these same lessons — that's the Method.

The Circle Method sketched on a napkin: get your 2, help your 2 get theirs, teach them to do the same diff --git a/server.js b/server.js index 2f3cf67..d70cafa 100644 --- a/server.js +++ b/server.js @@ -91,7 +91,7 @@ FACTS: - "What if I can't afford the upgrades?": upgrades are optional and self-paced; never use money you can't afford to lose; the first payments at each level are designed to help fund the next upgrade, so many let team activity pay their way up (see https://rmcircle.team/how-pay-works). - "My prospect wants to join but has no money": coach the member to sort which of three objections it really is. (1) Most common — "sounds expensive/complicated": entry is ${c.premiumEntryPol || 362} POL — right now roughly ${entryUsdLine()} — payable with a regular debit card via the join page; many prospects imagine far more. ALWAYS quote the current dollar figure from this prompt, never a remembered one, and add that POL's price moves. (2) Priority dressed as money: send the "Pocket Change" curiosity video from /tools and go silent (it calls out weekly wasted spending — fully evergreen, no dollar figures) — never argue it. (3) Genuinely broke: the honest answer is "don't join yet" — never rent money, never desperation recruiting; ask "when's payday?" and follow up then. Waiting costs the prospect nothing: invite links don't expire and the team's growth is publicly visible on-chain while they wait. HARD RULE: nobody ever fronts or loans someone's entry — not the sponsor, not the team; it kills commitment and creates one person controlling positions registered to others' wallets. Teach-forward: have the member log how each conversation goes, because their people will hit the same objection. - "Someone below me won't upgrade / is stalled": a paused member NEVER blocks money. Each person below you pays you separately, once, at one level — nothing waits on pairs or chains of upgrades. Payments a paused member can't catch pass straight UP through them to the next ready position (often the asker's), and everyone below the paused member keeps flowing normally; the only thing on hold is the single payment that person would send at the asker's matching level. Never pressure anyone — paused members keep collecting entry rewards from new team growth and often return once the upgrade funds itself. Point the asker at the pipeline panel on their dashboard to see who IS moving, and (teach-forward) have them show their two the same panel. - - "Is there a members area?": YES — a position unlocks: the Circle Method course (10 lessons + the real cash-out walkthrough) in the training members area, the live coaching dashboard at /my/, promo tools personalized with their link at /tools, printable playbooks, wallet-verified team messaging, and the Telegram companion bot + Mini App. Access is by the wallet that owns the position — one free signature, no passwords — or automatic inside the Mini App. + - "Is there a members area?": YES — a position unlocks: the Circle Method course (10 lessons + the real cash-out walkthrough) in the training members area, the weekly team webinar replays at https://rmcircle.team/replays, the live coaching dashboard at /my/, promo tools personalized with their link at /tools, printable playbooks, wallet-verified team messaging, and the Telegram companion bot + Mini App. Access is by the wallet that owns the position — one free signature, no passwords — or automatic inside the Mini App. - "What's the actual product? / What do I get?": answer with the business-in-a-box inventory that comes with a position on THIS team: (1) The Circle Method — a complete 10-lesson recruiting & coaching course in the members area (Lesson 1 is a free preview; the rest unlocks with your position); (2) a promo arsenal — 4 professionally produced curiosity videos with matched landing pages, ready-made posts, descriptions, email swipes and a full banner kit, all auto-personalized with their invite link; (3) printable playbooks (48-Hour Fast Start + Weekly Rhythm) with their own QR code; (4) an AI coach on every page answering in 21+ languages around the clock; (5) a live coaching dashboard reading the blockchain — pipeline, org bar, coach panel, wallet-verified team messaging. Be clear this toolkit is provided by this team around the public smart contract; the contract itself is the payment engine, the toolkit is what this team adds. - "I'm not a salesperson / I don't know anyone": there is nothing to sell — share a short video and an invite link; the site explains it; you need a couple of curious people, not a big network; ready-made promo tools are at https://rmcircle.team/tools. - "What if people quit?": positions live on-chain and don't vanish when someone goes quiet; the team keeps building around the spot; focus on a few real builders, not volume. No income is guaranteed. @@ -487,7 +487,7 @@ async function handleTranslate(req,res){ // frame-src includes 'self' so our own pages (the Page Builder preview framing // a member's /p/ page) are not blocked by our own policy. -const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src 'self' https://www.youtube-nocookie.com"; +const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; media-src 'self' https://coolify-saasytop.nyc3.digitaloceanspaces.com; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src 'self' https://www.youtube-nocookie.com"; function securityHeaders(extra={}) { // Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS. return { @@ -942,6 +942,17 @@ async function handleApi(req,res,pathname){ }catch(err){ return json(res,502,{error:String(err.message||err)}); } } + // ── Webinar replays (members area) ─────────────────────────────────────── + // The list lives in DATA_DIR/replays.json so adding the weekly replay never + // needs a deploy. A locked response still returns titles/dates — prospects + // see the shape of the product — but never the video URLs. + if(req.method==='GET'&&pathname==='/api/public/replays'){ + let list=[]; try{ list=readJson(path.join(DATA_DIR,'replays.json'))||[]; }catch(e){} + const member=messages.authFromCookie(req); + if(!member)return json(res,200,{locked:true,replays:list.map(r=>({title:r.title,date:r.date,minutes:r.minutes}))},{'Cache-Control':'no-store'}); + return json(res,200,{locked:false,id:member.id,replays:list},{'Cache-Control':'no-store'}); + } + // ── Traffic Desk ───────────────────────────────────────────────────────── if(req.method==='GET'&&pathname==='/api/public/suite-traffic'){ const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup.',code:500})); @@ -1593,7 +1604,7 @@ const server=http.createServer(async(req,res)=>{ if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]); } let file; - if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/wallet-check'||pathname==='/wallet-check/')file=path.join(PUBLIC_DIR,'wallet-check.html');else if(pathname==='/suite/brand'||pathname==='/suite/brand/')file=path.join(PUBLIC_DIR,'suite-brand.html');else if(pathname==='/suite/grants'||pathname==='/suite/grants/')file=path.join(PUBLIC_DIR,'suite-grants.html');else if(pathname==='/suite/intel'||pathname==='/suite/intel/')file=path.join(PUBLIC_DIR,'suite-intel.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ + if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/replays'||pathname==='/replays/')file=path.join(PUBLIC_DIR,'replays.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/suite/founder'||pathname==='/suite/founder/')file=path.join(PUBLIC_DIR,'suite-founder.html');else if(pathname==='/wallet-check'||pathname==='/wallet-check/')file=path.join(PUBLIC_DIR,'wallet-check.html');else if(pathname==='/suite/brand'||pathname==='/suite/brand/')file=path.join(PUBLIC_DIR,'suite-brand.html');else if(pathname==='/suite/grants'||pathname==='/suite/grants/')file=path.join(PUBLIC_DIR,'suite-grants.html');else if(pathname==='/suite/intel'||pathname==='/suite/intel/')file=path.join(PUBLIC_DIR,'suite-intel.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{ const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file=''; } if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);