account: server-side plan lookup via /api/my-plan, scrub internal comments from JS

The /account "Loading..." spinner stayed on forever because the
browser-side waitlist read went through the anon key and didn't reach
the row. Replaced it with a POST /api/my-plan: the server verifies
the user's access_token via Supabase /auth/v1/user, then reads the
waitlist row with the service key. Bypasses RLS without exposing the
service key to the browser.

Stripped implementation comments from the served JS so the browser
doesn't broadcast how internals are shaped.

Build pipeline: declared the supabase_auth_user stub for both
build-local.sh and Dockerfile.stage so the bootstrap-injected forward
declarations match what's actually linked.
This commit is contained in:
Will Anderson
2026-05-01 23:46:26 -05:00
parent 4aa48538f6
commit eea9ff8ff4
5 changed files with 46 additions and 25 deletions
+10 -18
View File
@@ -1189,28 +1189,20 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String {
async function loadWaitlistData(email) {
try {
var result = await sb
.from('waitlist')
.select('plan, member_number, source, created_at')
.eq('email', email)
.order('created_at', { ascending: false })
.limit(1);
var sess = await sb.auth.getSession();
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
if (!token) { showNoPlan(); return; }
if (result.error) {
console.error('Waitlist query error:', result.error.message);
showNoPlan();
return;
}
var r = await fetch('/api/my-plan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ access_token: token })
});
var row = await r.json();
var row = result.data && result.data.length > 0 ? result.data[0] : null;
if (!row) {
// No plan — redirect to pricing so they can choose
showNoPlan();
return;
}
if (!row || !row.plan) { showNoPlan(); return; }
renderPlanCard(row);
} catch (e) {
console.error('Failed to load waitlist data:', e);
showNoPlan();
}
}
-5
View File
@@ -703,7 +703,6 @@ fn checkout_page(plan: String, pub_key: String) -> String {
return;
}
// Capture the PI id so we can attach a Customer at submit time
window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : '');
waitForStripe(function() {
@@ -818,10 +817,6 @@ fn checkout_page(plan: String, pub_key: String) -> String {
setLoading(true);
document.getElementById('payment-message').style.display = 'none';
// Link a Stripe Customer to this PaymentIntent and to the Supabase waitlist row
// (so the buyer shows up as a named customer, not Guest, and the account page
// can find their plan via stripe_customer_id). Non-blocking - if it fails, the
// webhook still links them server-side after payment_intent.succeeded fires.
if (window._neuronPiId) {
try {
await fetch('/api/link-customer', {
+34
View File
@@ -498,6 +498,40 @@ fn handle_request(method: String, path: String, body: String) -> String {
return "{\"status\":\"ok\",\"service\":\"neuron-web\"}"
}
// My plan: server-side waitlist read with JWT verification
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
// /auth/v1/user, extract the email, then read the waitlist row with the
// SERVICE key (bypasses RLS). Canonical plan source for /account.
if str_eq(path, "/api/my-plan") {
let mp_jwt: String = json_get_string(body, "access_token")
if str_eq(mp_jwt, "") {
return "{\"__status__\":401,\"error\":\"missing_jwt\"}"
}
let mp_sb_url: String = state_get("__supabase_project_url__")
let mp_anon: String = state_get("__supabase_anon_key__")
let mp_service: String = state_get("__supabase_service_key__")
if str_eq(mp_sb_url, "") || str_eq(mp_anon, "") || str_eq(mp_service, "") {
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
}
let mp_user: String = supabase_auth_user(mp_sb_url, mp_anon, mp_jwt)
let mp_email: String = json_get(mp_user, "email")
if str_eq(mp_email, "") {
return "{\"__status__\":401,\"error\":\"invalid_jwt\"}"
}
let mp_email_safe: String = str_replace(str_replace(mp_email, "@", "%40"), "+", "%2B")
let mp_query: String = "waitlist?select=plan,member_number,source,created_at,stripe_customer_id,name&email=eq." + mp_email_safe + "&order=created_at.desc&limit=1"
let mp_resp: String = supabase_get(mp_sb_url, mp_service, mp_query)
if str_eq(mp_resp, "") || str_eq(mp_resp, "[]") {
return "{\"plan\":null,\"email\":\"" + mp_email + "\"}"
}
// Strip outer array brackets to return a plain object
if str_starts_with(mp_resp, "[") {
let mp_inner: String = str_slice(mp_resp, 1, str_len(mp_resp) - 1)
return mp_inner
}
return mp_resp
}
// Founding count
if str_eq(path, "/api/founding-count") {