Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce5f4a8be | |||
| 37c7dca30d | |||
| 73c435eb90 | |||
| 7be2b49300 | |||
| e5c05cbece | |||
| c7f4d0248c | |||
| 9feb9e24b6 | |||
| a346a2197e | |||
| 54d48ed679 |
+63
-2
@@ -77,6 +77,23 @@ static _Thread_local int _tl_arena_active = 0;
|
||||
* Allows serving PNGs and other binary files without strlen truncation. */
|
||||
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) {
|
||||
if (!_tl_arena_active || !p) return;
|
||||
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;
|
||||
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
|
||||
* with embedded null bytes — PNG, WOFF2, etc.). Fall back to strlen for
|
||||
* 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 */
|
||||
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);
|
||||
free(env_body);
|
||||
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 {
|
||||
@@ -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);
|
||||
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
|
||||
* 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)
|
||||
@@ -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. */
|
||||
_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);
|
||||
jb_puts(&out, EL_HTTP_RESPONSE_TAG); /* {"el_http_response":1 */
|
||||
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, hj);
|
||||
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, '}');
|
||||
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 {
|
||||
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-size: clamp(1.5rem, 3vw, 2rem);
|
||||
font-weight: 600;
|
||||
|
||||
+3
-17
@@ -1,20 +1,6 @@
|
||||
// checkout-free.el -- Free plan: show success panel after auth completes.
|
||||
// Watches the auth-badge element; when it becomes visible, hides the auth
|
||||
// section and shows the free-success panel. No card required for free tier.
|
||||
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
||||
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe
|
||||
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age
|
||||
// verification. This file is no longer compiled or loaded.
|
||||
|
||||
fn main() -> Void {
|
||||
native_js("(function() {
|
||||
var success = document.getElementById('free-success');
|
||||
var auth = document.getElementById('auth-section');
|
||||
if (!success) return;
|
||||
var timer = setInterval(function() {
|
||||
var badge = document.getElementById('auth-badge');
|
||||
if (badge && badge.offsetParent !== null) {
|
||||
if (auth) auth.style.display = 'none';
|
||||
success.style.display = '';
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 150);
|
||||
})()")
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ fn main() -> Void {
|
||||
if (submitLabel) {
|
||||
submitLabel.textContent = window._neuronMode === 'setup'
|
||||
? 'Save my card - no charge today →'
|
||||
: 'Complete purchase →';
|
||||
: PLAN === 'free' ? 'Verify age & get started →' : 'Complete purchase →';
|
||||
}
|
||||
waitForStripe(function() {
|
||||
if (!stripe) stripe = Stripe(STRIPE_PK);
|
||||
|
||||
+11
-13
@@ -697,23 +697,21 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
}
|
||||
}
|
||||
|
||||
// Free tier: creates a SetupIntent for age verification (18+ requirement).
|
||||
// No charge — but the user must provide a valid payment method.
|
||||
// Free tier: $0 PaymentIntent for age verification (18+ requirement).
|
||||
// Verifies card is valid. No charge, no capture.
|
||||
// Note: setup_future_usage cannot be used with amount=0.
|
||||
if str_eq(plan, "free") {
|
||||
let free_si_body: String = "automatic_payment_methods[enabled]=true"
|
||||
+ "&usage=off_session"
|
||||
let free_pi_body: String = "amount=0"
|
||||
+ "¤cy=usd"
|
||||
+ "&payment_method_types[]=card"
|
||||
+ "&metadata[plan]=free"
|
||||
+ "&metadata[purpose]=age_verification"
|
||||
let free_si_body = if !str_eq(pi_cus_id, "") { free_si_body + "&customer=" + pi_cus_id } else { free_si_body }
|
||||
let free_si_resp: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/setup_intents",
|
||||
free_si_body,
|
||||
let free_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
|
||||
let free_pi_resp: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/payment_intents",
|
||||
free_pi_body,
|
||||
auth_header)
|
||||
if str_starts_with(free_si_resp, "{") {
|
||||
let inner: String = str_slice(free_si_resp, 1, str_len(free_si_resp))
|
||||
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
|
||||
}
|
||||
return free_si_resp
|
||||
return free_pi_resp
|
||||
}
|
||||
|
||||
// Setup-mode path: save payment method, do not charge. Only valid
|
||||
|
||||
Reference in New Issue
Block a user