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. */
|
* 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;
|
||||||
|
|||||||
+3
-17
@@ -1,20 +1,6 @@
|
|||||||
// checkout-free.el -- Free plan: show success panel after auth completes.
|
// checkout-free.el -- RETIRED. Free plan now uses the standard Stripe
|
||||||
// Watches the auth-badge element; when it becomes visible, hides the auth
|
// payment flow (checkout-stripe.el) with a $0 PaymentIntent for age
|
||||||
// section and shows the free-success panel. No card required for free tier.
|
// verification. This file is no longer compiled or loaded.
|
||||||
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
|
||||||
|
|
||||||
fn main() -> Void {
|
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) {
|
if (submitLabel) {
|
||||||
submitLabel.textContent = window._neuronMode === 'setup'
|
submitLabel.textContent = window._neuronMode === 'setup'
|
||||||
? 'Save my card - no charge today →'
|
? 'Save my card - no charge today →'
|
||||||
: 'Complete purchase →';
|
: PLAN === 'free' ? 'Verify age & get started →' : 'Complete purchase →';
|
||||||
}
|
}
|
||||||
waitForStripe(function() {
|
waitForStripe(function() {
|
||||||
if (!stripe) stripe = Stripe(STRIPE_PK);
|
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).
|
// Free tier: $0 PaymentIntent for age verification (18+ requirement).
|
||||||
// No charge — but the user must provide a valid payment method.
|
// Verifies card is valid. No charge, no capture.
|
||||||
|
// Note: setup_future_usage cannot be used with amount=0.
|
||||||
if str_eq(plan, "free") {
|
if str_eq(plan, "free") {
|
||||||
let free_si_body: String = "automatic_payment_methods[enabled]=true"
|
let free_pi_body: String = "amount=0"
|
||||||
+ "&usage=off_session"
|
+ "¤cy=usd"
|
||||||
|
+ "&payment_method_types[]=card"
|
||||||
+ "&metadata[plan]=free"
|
+ "&metadata[plan]=free"
|
||||||
+ "&metadata[purpose]=age_verification"
|
+ "&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_pi_body = if !str_eq(pi_cus_id, "") { free_pi_body + "&customer=" + pi_cus_id } else { free_pi_body }
|
||||||
let free_si_resp: String = http_post_form_auth(
|
let free_pi_resp: String = http_post_form_auth(
|
||||||
"https://api.stripe.com/v1/setup_intents",
|
"https://api.stripe.com/v1/payment_intents",
|
||||||
free_si_body,
|
free_pi_body,
|
||||||
auth_header)
|
auth_header)
|
||||||
if str_starts_with(free_si_resp, "{") {
|
return free_pi_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup-mode path: save payment method, do not charge. Only valid
|
// Setup-mode path: save payment method, do not charge. Only valid
|
||||||
|
|||||||
Reference in New Issue
Block a user