Paste this whole file into Cursor. It is self-contained.
./scripts/agent.sh sign-in
./scripts/agent.sh claim <slug> "what you'll do"
./scripts/agent.sh boot
fedm8 (1099:cursor-agent, lease to 2026-08-12 08:46). Task 1 is yours because of that lease.release when done. One claim = one billable session; never double-post.| Lane | Repos | Rule |
|---|---|---|
| Gated | nodedough, fedm8, fedm8-scan, cipherdeck, cipherdeck-apps | PR → verify on test → chat approve → promote to production |
| Ungated (PROD-IT) | jbnx.io/projects-portal, jbnx-bill, mkt, landing pages | main IS production. Push deploys live. Verify the live URL. |
production branches · you hold this claimproduction branches were created today on nodedough, cipherdeck and cipherdeck-apps. The two FedM8 repos were skipped because you hold the lease.
for r in fedm8 fedm8-scan; do
sha=$(gh api repos/jbnx/$r/git/ref/heads/main --jq .object.sha)
gh api repos/jbnx/$r/git/refs -f ref=refs/heads/production -f sha="$sha"
done
Verify: gh api repos/jbnx/fedm8/git/ref/heads/production --jq .object.sha
Then tell Joey — the Railway half is dashboard-only (update-service excludes source changes). For all five gated services: Settings → Source → "Branch connected to production" → main → production, leaving test on main. Until that is done the branches are inert and the gate in v36 is still not real.
projects-portal/server.js · PROD-ITRepo jbnx/jbnx.io, path projects-portal/server.js. Push to main — that is production for this repo.
Measured: projects.jbnx.io returns 371,068 B with no Content-Encoding even when the request offers br,gzip,deflate. The source has no zlib and no accept-encoding handling in 4,336 lines. It is a raw Node http server with 22 writeHead() call sites and 0 setHeader() — so wrap the response once, do not patch 22 sites.
The portal serves a live /api/events EventSource feed. A normal compression wrapper buffers the body until end() and permanently breaks SSE — the feed hangs open and never flushes. The wrapper must bail out on (a) the SSE path and (b) any response that calls res.write() instead of a single res.end(body).
Add with the other requires:
const zlib = require('zlib');
Add above the request handler:
// Compress single-shot responses. Bails out on streaming/SSE responses,
// which must not be buffered. 2026-08-11 (estate review finding 3).
function enableCompression(req, res) {
const ae = String(req.headers['accept-encoding'] || '');
const enc = /\bbr\b/.test(ae) ? 'br' : /\bgzip\b/.test(ae) ? 'gzip' : null;
if (!enc) return;
if (String(req.url || '').startsWith('/api/events')) return; // never buffer SSE
const origWriteHead = res.writeHead.bind(res);
const origWrite = res.write.bind(res);
const origEnd = res.end.bind(res);
let code = 200, hdrs = null, streamed = false, headSent = false;
const flushHead = () => { if (!headSent && hdrs) { headSent = true; origWriteHead(code, hdrs); } };
res.writeHead = function (statusCode, headers) { code = statusCode; hdrs = headers || {}; return res; };
res.write = function (...args) { streamed = true; flushHead(); return origWrite(...args); };
res.end = function (body, ...rest) {
if (streamed || !body || typeof body === 'function') { flushHead(); return origEnd(body, ...rest); }
const ct = String((hdrs && (hdrs['Content-Type'] || hdrs['content-type'])) || '');
const worthIt = /^(text\/|application\/(json|javascript|xml)|image\/svg)/.test(ct)
&& Buffer.byteLength(body) > 1024;
if (!worthIt) { flushHead(); return origEnd(body, ...rest); }
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
const compress = enc === 'br' ? zlib.brotliCompress : zlib.gzip;
compress(buf, (err, out) => {
if (err) { flushHead(); return origEnd(body, ...rest); }
hdrs['Content-Encoding'] = enc;
hdrs['Vary'] = 'Accept-Encoding';
hdrs['Content-Length'] = out.length;
headSent = true;
origWriteHead(code, hdrs);
origEnd(out);
});
};
}
First line inside the http.createServer(...) handler:
enableCompression(req, res);
curl -sI -H 'Accept-Encoding: br,gzip' https://projects.jbnx.io | grep -i content-encoding # expect br
curl -s -H 'Accept-Encoding: br,gzip' https://projects.jbnx.io -o /dev/null -w '%{size_download}\n' # expect ~60–80k (was 371,068)
curl -s https://hi.jbnx.io/json | head -c 40 # expect version 36
curl -s -o /dev/null -w '%{http_code}\n' https://projects.jbnx.io/healthz # expect 200
curl -N -m 8 -H 'Accept: text/event-stream' https://projects.jbnx.io/api/events | head -c 200 # MUST stream, not hang
If the SSE check hangs, revert immediately. That feed is what every portal client depends on, and this service also serves hi.jbnx.io — breaking it strands every agent including you.
jbnx-bill · PROD-ITRepo jbnx/jbnx-bill, server.js (2,167 lines, 6 writeHead, 2 setHeader).
Measured: bill.jbnx.io sends no HSTS, no CSP, no X-Frame-Options, no X-Content-Type-Options, no Referrer-Policy. It is the customer login and payment portal, so it is framable and clickjackable right now.
Do not use X-Frame-Options: DENY. The service carries BILL_SSO_FRAME_ANCESTORS — framing is intentional for SSO. Use CSP frame-ancestors driven by that variable; it is currently enforcing nothing.
// Security headers. 2026-08-11 (estate review finding 2).
function securityHeaders(res) {
const ancestors = process.env.BILL_SSO_FRAME_ANCESTORS || "'none'";
res.setHeader('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'same-origin');
res.setHeader('Content-Security-Policy', `frame-ancestors ${ancestors}`);
}
Call securityHeaders(res); first in the request handler.
⚠️ includeSubDomains on jbnx.io affects every *.jbnx.io host. All 17 probed on 2026-08-11 already serve HTTPS, so this should be safe — confirm nothing is intentionally plain HTTP first.
curl -sI https://bill.jbnx.io | grep -iE 'strict-transport|content-security|x-content-type|referrer'
curl -s -o /dev/null -w '%{http_code}\n' https://bill.jbnx.io
# then log in once by hand and confirm SSO framing still works
While you're in there: CREDITS_PER_HOUR appears 9 times in this file. The directive says it lives in 4 places and that a figure off by 100× is a defect. Every occurrence should reference one const CREDITS_PER_HOUR = 6000;, never a repeated literal.
Apply the same header block to jbnx/cipherdeck-apps and the jbnx.io landing service — both also send nothing. nodedough.com is the reference implementation; copy its set.
cipherdeck.com has no <h1> · gated (TEST-IT)Repo jbnx/cipherdeck, index.html — 179 lines, 7,826 bytes, <h1> count = 0, first heading is <h2>Where to shop.
Add a real <h1> as the opening heading. Keep the existing <h2>s as section headings. If the design cannot change, minimum acceptable:
<h1 class="sr-only">CipherDeck — graded trading cards, shipped protected and tracked</h1>
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
clip:rect(0,0,0,0);white-space:nowrap;border:0}
Prefer a visible <h1>. Verify: curl -s https://cipherdeck.com | grep -c '<h1' → 1.
cipherdeck-apps accessibility · gated (TEST-IT)Measured on the live 745 KB bundle: 35 icon-only <button> elements with no accessible name, and ~6 <img> built inside JS template strings with no alt. Screen-reader users hear "button" 35 times on a paid product.
aria-label="<the action>".<img> → alt="" if decorative, a real description otherwise.Measured: ai.fedm8.com and scan.fedm8.com both return 200 with the same 37,193 B bundle and neither redirects to the other. Neither publishes terms, privacy or refunds. Neither sends any security header. This is the product handling veteran PII, CAGE/UEI and Stripe.
/terms, /privacy, /refunds. NodeDough's /legal/* is the model.Handling veteran PII with no published privacy notice is a compliance problem independent of whether you're charging yet.
The directive is a database row (agent_ops.policy key project_llm_directive), and it was updated to v36 today. Its source still points at sql/directive-v35.md, so the repo copy is stale and the runbook's "confirm /md matches the repo source file" check would now fail.
curl -s https://hi.jbnx.io/md > sql/directive-v36.mdsql/directive-v35.md. update agent_ops.policy
set value = value - 'source_drift'
|| jsonb_build_object('source','sql/directive-v36.md')
where key = 'project_llm_directive';
Backup of v35 is at policy key project_llm_directive_backup_v35 — leave it.
1. Revoking function EXECUTE takes both halves. This reports success and changes nothing:
revoke execute on function public.f() from anon, authenticated; -- anon still inherits via PUBLIC
Postgres grants EXECUTE to PUBLIC by default and Supabase grants it to the roles directly. You need:
revoke execute on function public.f() from public, anon, authenticated;
Always re-check has_function_privilege afterwards — asserting "no exception" is not asserting the effect.
2. Never blanket set search_path = ''. It breaks any function whose body uses unqualified names. Pin to the function's own schema instead: alter function s.f() set search_path = s, public;
3. Check the call site before revoking from anon. validate_invite_code on NodeDough must stay anon-callable — signup calls it before the user authenticates. An audit recommended revoking it; that would have broken signup.
update_recurring overload droppedsearch_path pinned on 49 jbnx functionsjbnx-ops runbook rewrittenproduction branches created on nodedough, cipherdeck, cipherdeck-appsFull detail: /framework/estate-review-2026-08-11, /framework/estate-review-remediation-2026-08-11, /framework/patch-pack-2026-08-11.