Founder Desk (L8): weekly campaign pack + Suite API

Completes the ladder — no tier is a placeholder now.

Every tier below builds one thing at a time, which is right for someone
still finding their words. At the top the constraint is different: these are
people running organisations who don't have an afternoon to spend clicking.
So one pass produces five social posts across five different angles, three
outreach messages (first approach, follow-up, pyramid-scheme answer), an
email, and three text ads — all carrying their link, all in the member's own
voice profile if they have one.

Partial failures are reported rather than hidden: if the engine can't finish
a section the pack says which, instead of quietly handing over a short week.

API access is real, not a label: a personal key over
GET /me, GET /team, POST /generate. Keys are stored hashed and shown exactly
once, compared in constant time, and every call goes through the same meters
and compliance rules as the web tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 11:09:48 -05:00
parent 85261f2719
commit f05d5a2746
7 changed files with 514 additions and 4 deletions
+62 -1
View File
@@ -28,6 +28,7 @@ const suiteTextAds = require('./suite-textads');
const suiteVoice = require('./suite-voice'); suiteVoice.init({ dataDir: DATA_DIR });
const suiteSplit = require('./suite-split'); suiteSplit.init({ dataDir: DATA_DIR });
const suiteLeader = require('./suite-leader');
const suiteFounder = require('./suite-founder'); suiteFounder.init({ dataDir: DATA_DIR });
const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000;
@@ -959,6 +960,66 @@ async function handleApi(req,res,pathname){
}
}
// -- Founder Desk (level 8) -----------------------------------------------
if(pathname==='/api/public/suite-founder'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<suiteFounder.MIN_LEVEL)return json(res,403,{error:'The Founder Desk unlocks at Corona (level 8).',minLevel:suiteFounder.MIN_LEVEL});
if(req.method==='GET'){
return json(res,200,{pack:suiteFounder.loadPack(e.d.id),key:suiteFounder.keyInfo(e.d.id),
level:e.d.level,id:e.d.id,writerReady:suiteAI.configured(),
meter:suiteMeter.check(e.d.id,e.d.level,'founder')});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.issueKey){ return json(res,200,{key:suiteFounder.issueKey(e.d.id),info:suiteFounder.keyInfo(e.d.id)}); }
if(b.revokeKey){ suiteFounder.revokeKey(e.d.id); return json(res,200,{info:suiteFounder.keyInfo(e.d.id)}); }
if(!suiteAI.configured())return json(res,503,{error:'The writer is warming up - try again shortly.'});
const gate=suiteMeter.check(e.d.id,e.d.level,'founder');
if(!gate.allowed)return json(res,429,{error:'You have used all '+gate.limit+' campaign packs this month. It resets on the 1st.',meter:gate});
try{
const pack=await suiteFounder.buildPack({id:e.d.id,voice:suiteVoice.promptBlock(e.d.id)});
suiteMeter.record(e.d.id,'founder',1);
return json(res,200,{pack:pack,meter:suiteMeter.check(e.d.id,e.d.level,'founder')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
}
// -- Founder API v1: key-authenticated, read-mostly ------------------------
if(pathname.startsWith('/api/suite/v1/')){
const auth=String(req.headers['authorization']||'');
const raw=auth.replace(/^Bearer\s+/i,'').trim();
const memberId=suiteFounder.memberForKey(raw);
if(!memberId)return json(res,401,{error:'Provide your Founder Desk API key as: Authorization: Bearer <key>'});
let md=null;
try{ md=await chain.memberPublic(memberId); }catch(err){}
if(!md||!md.registered)return json(res,404,{error:'Position not found.'});
if(pathname==='/api/suite/v1/me'&&req.method==='GET'){
return json(res,200,{id:memberId,level:md.level,levelName:md.levelName,tier:md.tier,
directCount:md.directCount,qualified:(md.directCount||0)>=2,
totalEarnedPol:md.totalEarnedPol,link:'https://rmcircle.team/join/'+memberId});
}
if(pathname==='/api/suite/v1/team'&&req.method==='GET'){
return json(res,200,{scan:suiteLeader.scan(memberId,25)});
}
if(pathname==='/api/suite/v1/generate'&&req.method==='POST'){
const b=await bodyJson(req)||{};
const kind=String(b.kind||'post');
if(!suiteAI.KINDS[kind])return json(res,400,{error:'Unknown kind. One of: '+Object.keys(suiteAI.KINDS).join(', ')});
const brief=String(b.brief||'').trim().slice(0,1200);
if(brief.length<3)return json(res,400,{error:'Send a brief describing what the piece is about.'});
const gate=suiteMeter.check(memberId,md.level,'copy');
if(!gate.allowed)return json(res,429,{error:'Monthly generation allowance used.',meter:gate});
try{
const text=await suiteAI.generate(kind,brief,{link:'https://rmcircle.team/join/'+memberId,id:memberId,voice:suiteVoice.promptBlock(memberId)});
suiteMeter.record(memberId,'copy',1);
return json(res,200,{text:text,meter:suiteMeter.check(memberId,md.level,'copy')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
return json(res,404,{error:'Unknown endpoint. Available: GET /api/suite/v1/me, GET /api/suite/v1/team, POST /api/suite/v1/generate'});
}
if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
@@ -1303,7 +1364,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==='/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==='/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==='/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);