Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f6df973cb | |||
| be849c608e | |||
| 5ce5f4a8be | |||
| 6e425da63e |
+63
-2
@@ -77,6 +77,23 @@ static _Thread_local int _tl_arena_active = 0;
|
|||||||
* Allows serving PNGs and other binary files without strlen truncation. */
|
* Allows serving PNGs and other binary files without strlen truncation. */
|
||||||
static _Thread_local size_t _tl_fs_read_len = 0;
|
static _Thread_local size_t _tl_fs_read_len = 0;
|
||||||
|
|
||||||
|
/* Binary body side-channel for http_response().
|
||||||
|
*
|
||||||
|
* http_response() normally JSON-encodes the body via jb_emit_escaped(), which
|
||||||
|
* stops at the first null byte (C-string semantics). Binary files like PNGs
|
||||||
|
* contain null bytes as early as byte 8 (IHDR chunk length), causing truncation.
|
||||||
|
*
|
||||||
|
* When _tl_fs_read_len > 0 at the time http_response() is called, we skip
|
||||||
|
* JSON-encoding and instead:
|
||||||
|
* 1. malloc-copy the raw bytes here
|
||||||
|
* 2. write the sentinel string "__el_binary__" into the envelope body field
|
||||||
|
* 3. In http_send_response(), detect the sentinel and use these raw bytes
|
||||||
|
*
|
||||||
|
* Thread-local so each worker thread has independent storage.
|
||||||
|
* Lifecycle: set by http_response(), consumed (and freed) by http_send_response(). */
|
||||||
|
static _Thread_local char* _tl_binary_body = NULL;
|
||||||
|
static _Thread_local size_t _tl_binary_size = 0;
|
||||||
|
|
||||||
static void el_arena_track(char* p) {
|
static void el_arena_track(char* p) {
|
||||||
if (!_tl_arena_active || !p) return;
|
if (!_tl_arena_active || !p) return;
|
||||||
if (_tl_arena.count >= _tl_arena.cap) {
|
if (_tl_arena.count >= _tl_arena.cap) {
|
||||||
@@ -1536,10 +1553,22 @@ static void http_send_response(int fd, const char* body) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* eff_body = is_envelope ? env_body : body;
|
const char* eff_body = is_envelope ? env_body : body;
|
||||||
|
int binary_side_channel = 0;
|
||||||
|
|
||||||
|
/* Binary side-channel: if the envelope body is the sentinel "__el_binary__",
|
||||||
|
* http_response() stored the real bytes in _tl_binary_body/_tl_binary_size.
|
||||||
|
* Substitute them here so http_send_all() sends the correct binary payload. */
|
||||||
|
if (is_envelope && env_body && strcmp(env_body, "__el_binary__") == 0
|
||||||
|
&& _tl_binary_body && _tl_binary_size > 0) {
|
||||||
|
eff_body = _tl_binary_body;
|
||||||
|
binary_side_channel = 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Use the real byte count from fs_read if available (handles binary files
|
/* Use the real byte count from fs_read if available (handles binary files
|
||||||
* with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for
|
* with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for
|
||||||
* normal text/JSON responses where _tl_fs_read_len is 0. */
|
* normal text/JSON responses where _tl_fs_read_len is 0. */
|
||||||
size_t blen = (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
|
size_t blen = binary_side_channel ? _tl_binary_size
|
||||||
|
: (_tl_fs_read_len > 0) ? _tl_fs_read_len : strlen(eff_body);
|
||||||
_tl_fs_read_len = 0; /* consume — one-shot per response */
|
_tl_fs_read_len = 0; /* consume — one-shot per response */
|
||||||
int head_only = _tl_http_head_only;
|
int head_only = _tl_http_head_only;
|
||||||
|
|
||||||
@@ -1587,6 +1616,13 @@ static void http_send_response(int fd, const char* body) {
|
|||||||
if (env_parsed_root) el_release(env_parsed_root);
|
if (env_parsed_root) el_release(env_parsed_root);
|
||||||
free(env_body);
|
free(env_body);
|
||||||
free(hdrs.buf);
|
free(hdrs.buf);
|
||||||
|
|
||||||
|
/* Release binary side-channel if it was used (or left over from an error). */
|
||||||
|
if (_tl_binary_body) {
|
||||||
|
free(_tl_binary_body);
|
||||||
|
_tl_binary_body = NULL;
|
||||||
|
_tl_binary_size = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -1961,6 +1997,14 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
|||||||
const char* b = EL_CSTR(body);
|
const char* b = EL_CSTR(body);
|
||||||
if (!b) b = "";
|
if (!b) b = "";
|
||||||
|
|
||||||
|
/* Capture binary length BEFORE clearing _tl_fs_read_len.
|
||||||
|
* If the body came from fs_read(), _tl_fs_read_len holds the real byte
|
||||||
|
* count. jb_emit_escaped() stops at the first NUL byte, so we cannot
|
||||||
|
* JSON-encode binary data directly. Instead we copy it to a thread-local
|
||||||
|
* side-channel and write the sentinel "__el_binary__" into the envelope.
|
||||||
|
* http_send_response() detects the sentinel and uses the side-channel. */
|
||||||
|
size_t binary_len = _tl_fs_read_len;
|
||||||
|
|
||||||
/* Clear the fs_read binary-length hint: the envelope we're about to build
|
/* Clear the fs_read binary-length hint: the envelope we're about to build
|
||||||
* is a fresh JSON string, not the raw file bytes. Without this reset,
|
* is a fresh JSON string, not the raw file bytes. Without this reset,
|
||||||
* http_worker would use the stale _tl_fs_read_len (= original file size)
|
* http_worker would use the stale _tl_fs_read_len (= original file size)
|
||||||
@@ -1968,6 +2012,18 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
|||||||
* http_send_response and http_parse_envelope. */
|
* http_send_response and http_parse_envelope. */
|
||||||
_tl_fs_read_len = 0;
|
_tl_fs_read_len = 0;
|
||||||
|
|
||||||
|
if (binary_len > 0) {
|
||||||
|
/* Binary body path: store raw bytes in thread-local, emit sentinel. */
|
||||||
|
free(_tl_binary_body); /* discard any stale binary from a prior error path */
|
||||||
|
_tl_binary_body = malloc(binary_len);
|
||||||
|
if (_tl_binary_body) {
|
||||||
|
memcpy(_tl_binary_body, b, binary_len);
|
||||||
|
_tl_binary_size = binary_len;
|
||||||
|
} else {
|
||||||
|
_tl_binary_size = 0; /* malloc failed — fall through to empty body */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JsonBuf out; jb_init(&out);
|
JsonBuf out; jb_init(&out);
|
||||||
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
||||||
jb_puts(&out, ",\"status\":");
|
jb_puts(&out, ",\"status\":");
|
||||||
@@ -1977,7 +2033,12 @@ el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body) {
|
|||||||
jb_puts(&out, ",\"headers\":");
|
jb_puts(&out, ",\"headers\":");
|
||||||
jb_puts(&out, hj);
|
jb_puts(&out, hj);
|
||||||
jb_puts(&out, ",\"body\":");
|
jb_puts(&out, ",\"body\":");
|
||||||
jb_emit_escaped(&out, b);
|
if (binary_len > 0 && _tl_binary_body) {
|
||||||
|
/* Sentinel: http_send_response() will substitute the real bytes. */
|
||||||
|
jb_puts(&out, "\"__el_binary__\"");
|
||||||
|
} else {
|
||||||
|
jb_emit_escaped(&out, b);
|
||||||
|
}
|
||||||
jb_putc(&out, '}');
|
jb_putc(&out, '}');
|
||||||
return el_wrap_str(out.buf);
|
return el_wrap_str(out.buf);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -345,7 +345,31 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn checkout_style_html() -> String {
|
fn checkout_style_html() -> String {
|
||||||
let css: String = ".checkout-plan-name {
|
let css: String = ".checkout-shell {
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.checkout-summary {
|
||||||
|
position: sticky;
|
||||||
|
top: 2rem;
|
||||||
|
}
|
||||||
|
.checkout-form-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.checkout-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
.checkout-summary {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.checkout-plan-name {
|
||||||
font-family: var(--head);
|
font-family: var(--head);
|
||||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
+32
-20
@@ -142,14 +142,27 @@ fn main() -> Void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _todayUTC() { return Math.floor(Date.now() / 86400000); }
|
||||||
function loadSession() {
|
function loadSession() {
|
||||||
try {
|
try {
|
||||||
var s = localStorage.getItem('neuron_demo_session');
|
var s = localStorage.getItem('neuron_demo_session');
|
||||||
return s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
|
var parsed = s ? JSON.parse(s) : { messages: [], count: 0, context: '' };
|
||||||
|
// Reset count (and conversation) on new UTC day — keeps client in sync with server
|
||||||
|
var today = _todayUTC();
|
||||||
|
if (parsed.day !== today) {
|
||||||
|
parsed.count = 0;
|
||||||
|
parsed.messages = [];
|
||||||
|
parsed.greeted = false;
|
||||||
|
parsed.day = today;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
} catch(e) { return { messages: [], count: 0, context: '' }; }
|
} catch(e) { return { messages: [], count: 0, context: '' }; }
|
||||||
}
|
}
|
||||||
function saveSession(session) {
|
function saveSession(session) {
|
||||||
try { localStorage.setItem('neuron_demo_session', JSON.stringify(session)); } catch(e) {}
|
try {
|
||||||
|
if (!session.day) session.day = _todayUTC();
|
||||||
|
localStorage.setItem('neuron_demo_session', JSON.stringify(session));
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
function clearSession() {
|
function clearSession() {
|
||||||
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
|
try { localStorage.removeItem('neuron_demo_session'); } catch(e) {}
|
||||||
@@ -525,34 +538,33 @@ fn main() -> Void {
|
|||||||
// Server-side rate limit — show a live countdown to reset
|
// Server-side rate limit — show a live countdown to reset
|
||||||
if (d.rate_limited && d.reset_at) {
|
if (d.rate_limited && d.reset_at) {
|
||||||
var _showRateTimer = function() {
|
var _showRateTimer = function() {
|
||||||
var now = Math.floor(Date.now() / 1000);
|
var now = Math.floor(Date.now() / 1000);
|
||||||
var secsLeft = Math.max(0, d.reset_at - now);
|
var secsLeft = Math.max(0, d.reset_at - now);
|
||||||
var hh = Math.floor(secsLeft / 3600);
|
var hh = Math.floor(secsLeft / 3600);
|
||||||
var mm = Math.floor((secsLeft % 3600) / 60);
|
var mm = Math.floor((secsLeft % 3600) / 60);
|
||||||
var ss = secsLeft % 60;
|
var ss = secsLeft % 60;
|
||||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||||
var ts = hh > 0 ? (hh + ':' + pad(mm) + ':' + pad(ss)) : (pad(mm) + ':' + pad(ss));
|
return \"You've reached today's limit. Resets in \" + hh + ':' + pad(mm) + ':' + pad(ss) + '.';
|
||||||
return \"You've had 10 conversations today. Come back in \" + ts + \".\";
|
|
||||||
};
|
};
|
||||||
addMsg('ai', _showRateTimer());
|
addMsg('ai', _showRateTimer());
|
||||||
// Update the last ai message with a live ticker
|
// Update the bubble text with a live ticker
|
||||||
var _timerInterval = setInterval(function() {
|
var _timerInterval = setInterval(function() {
|
||||||
var thMsgsInner = document.getElementById('neuron-demo-msgs');
|
var msgsEl = document.getElementById('neuron-demo-messages');
|
||||||
if (!thMsgsInner) { clearInterval(_timerInterval); return; }
|
if (!msgsEl) { clearInterval(_timerInterval); return; }
|
||||||
var aiMsgs = thMsgsInner.querySelectorAll('.neuron-msg-ai');
|
var aiMsgs = msgsEl.querySelectorAll('.demo-msg-ai');
|
||||||
var lastAi = aiMsgs[aiMsgs.length - 1];
|
var lastAi = aiMsgs[aiMsgs.length - 1];
|
||||||
if (lastAi) { lastAi.textContent = _showRateTimer(); }
|
var lastBubble = lastAi ? lastAi.querySelector('.demo-msg-bubble') : null;
|
||||||
|
if (lastBubble) { lastBubble.textContent = _showRateTimer(); }
|
||||||
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
|
if (Math.floor(Date.now() / 1000) >= d.reset_at) {
|
||||||
clearInterval(_timerInterval);
|
clearInterval(_timerInterval);
|
||||||
if (lastAi) { lastAi.textContent = \"You're all set — conversations reset. Say hello!\"; }
|
if (lastBubble) { lastBubble.textContent = \"You're all set — conversations reset. Say hello!\"; }
|
||||||
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
if (input) { input.disabled = false; input.placeholder = 'Ask me anything...'; }
|
||||||
if (btn) { btn.disabled = false; }
|
if (btn) { btn.disabled = false; }
|
||||||
|
msgCount = 0; session.count = 0; session.day = _todayUTC(); saveSession(session); updateCountdown();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
|
if (input) { input.disabled = true; input.placeholder = 'Come back tomorrow...'; }
|
||||||
if (btn) { btn.disabled = true; }
|
if (btn) { btn.disabled = true; }
|
||||||
if (btn) { btn.disabled = false; }
|
|
||||||
if (input) { input.focus(); }
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
@@ -873,6 +873,32 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
|||||||
return "{\"rows\":" + ac_resp + "}"
|
return "{\"rows\":" + ac_resp + "}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Admin: reset all demo rate limits ────────────────────────────────────
|
||||||
|
// POST { "admin_token": "<NEURON_ADMIN_TOKEN>" }
|
||||||
|
// Deletes all rows from demo_rate_limits — resets every user's daily quota.
|
||||||
|
if str_eq(path, "/api/admin/reset-rate-limits") {
|
||||||
|
if !str_eq(method, "POST") {
|
||||||
|
return "{\"__status__\":405,\"error\":\"POST required\"}"
|
||||||
|
}
|
||||||
|
let rrl_token_in: String = json_get(body, "admin_token")
|
||||||
|
let rrl_token_exp: String = env("NEURON_ADMIN_TOKEN")
|
||||||
|
if str_eq(rrl_token_exp, "") {
|
||||||
|
return "{\"__status__\":503,\"error\":\"admin_token_not_configured\"}"
|
||||||
|
}
|
||||||
|
if !str_eq(rrl_token_in, rrl_token_exp) {
|
||||||
|
return "{\"__status__\":401,\"error\":\"unauthorized\"}"
|
||||||
|
}
|
||||||
|
let rrl_sb_url: String = state_get("__supabase_project_url__")
|
||||||
|
let rrl_sb_key: String = state_get("__supabase_service_key__")
|
||||||
|
if str_eq(rrl_sb_url, "") || str_eq(rrl_sb_key, "") {
|
||||||
|
return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}"
|
||||||
|
}
|
||||||
|
// DELETE /rest/v1/demo_rate_limits?uid=not.is.null (all rows)
|
||||||
|
let rrl_url: String = rrl_sb_url + "/rest/v1/demo_rate_limits?uid=not.is.null"
|
||||||
|
let _rrl_resp: String = http_delete_auth(rrl_url, rrl_sb_key, rrl_sb_key)
|
||||||
|
return "{\"ok\":true,\"message\":\"rate limits cleared\"}"
|
||||||
|
}
|
||||||
|
|
||||||
// ── My plan: server-side waitlist read with JWT verification ─────────────
|
// ── My plan: server-side waitlist read with JWT verification ─────────────
|
||||||
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
|
// POST { "access_token": "<user_jwt>" }. We verify the JWT via Supabase
|
||||||
// /auth/v1/user, extract the email, then read the waitlist row with the
|
// /auth/v1/user, extract the email, then read the waitlist row with the
|
||||||
|
|||||||
+3
-3
@@ -51,9 +51,9 @@ fn pricing_pro_features() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn pricing_founding_features() -> String {
|
fn pricing_founding_features() -> String {
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - founding member rate, priced below the major APIs")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Neuron Inference (Q3 2026) - pay-per-use at the founding member rate, below the major APIs")) +
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Everything in Professional - forever")) +
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Never pay again - lifetime updates included")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "No subscription — software updates are free forever")) +
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Founding member badge in the app")) +
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Private founding member community")) +
|
||||||
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
|
el_li("", el_span("class=\"dash\"", "-") + el_span("", "Shape the roadmap - your votes carry more weight")) +
|
||||||
@@ -125,7 +125,7 @@ fn pricing(sold: Int, total: Int) -> String {
|
|||||||
el_span("class=\"pricing-price\"", "$199") +
|
el_span("class=\"pricing-price\"", "$199") +
|
||||||
el_span("class=\"pricing-cadence\"", "lifetime")
|
el_span("class=\"pricing-cadence\"", "lifetime")
|
||||||
) +
|
) +
|
||||||
el_p("class=\"pricing-tagline\"", "Pay once. Everything, forever. Including Neuron Inference when it launches.") +
|
el_p("class=\"pricing-tagline\"", "Pay once for the platform — free software updates, forever. Inference is pay-per-use at your founding member rate.") +
|
||||||
spots_html +
|
spots_html +
|
||||||
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
|
el_ul("class=\"pricing-features\"", pricing_founding_features()) +
|
||||||
el_div("style=\"flex:1\"", "") +
|
el_div("style=\"flex:1\"", "") +
|
||||||
|
|||||||
Reference in New Issue
Block a user