#include #include #include #include #include #include #include "el_runtime.h" el_val_t http_get_auth(el_val_t url, el_val_t tok) { char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok)); el_val_t hdr_val = EL_STR(bearer); el_val_t headers = el_map_new(1, "Authorization", hdr_val); return http_get_with_headers(url, headers); } /* supabase_get — GET from Supabase REST API with both apikey and Bearer headers */ el_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query) { char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(service_key)); char url[2048]; snprintf(url, sizeof(url), "%s/rest/v1/%s", EL_CSTR(project_url), EL_CSTR(table_and_query)); el_val_t headers = el_map_new(3, "Authorization", EL_STR(bearer), "apikey", service_key, "Accept", EL_STR("application/json")); return http_get_with_headers(EL_STR(url), headers); } el_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body) { /* Bearer-only. No Content-Type — Stripe's REST API consumes * application/x-www-form-urlencoded bodies and is the heaviest * caller of this helper. JSON callers (Resend, Anthropic, etc.) must * use http_post_auth_json below, which sets Content-Type properly. * Mixing Stripe form-POST and Resend JSON-POST through one helper was * the root cause of the silent-drop bug — Resend 422s with "Missing * `to` field" if the body arrives without Content-Type: * application/json (it parses as form-encoded otherwise). */ char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok)); el_val_t hdr_val = EL_STR(bearer); el_val_t headers = el_map_new(1, "Authorization", hdr_val); return http_post_with_headers(url, body, headers); } /* http_post_auth_json — Bearer + Content-Type: application/json. * Use for any REST API that consumes JSON request bodies (Resend, * Anthropic, OpenAI, etc.). Returns the response body, or "" on * transport failure. HTTP 4xx/5xx are NOT swallowed — the body is * still returned so callers can parse error JSON. */ el_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body) { char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok)); el_val_t headers = el_map_new(2, "Authorization", EL_STR(bearer), "Content-Type", EL_STR("application/json")); return http_post_with_headers(url, body, headers); } /* * http_delete_auth - DELETE with Bearer auth + apikey (Supabase-style). * The runtime exposes http_delete(url) but no header variant, so we * implement DELETE-with-headers via http_post_with_headers using a * libcurl-backed helper; falling back to http_delete if no auth available. * * For Supabase REST DELETE we need both Authorization (user JWT) AND * apikey (anon key) - the apikey is required to reach PostgREST at all, * the Bearer JWT is what auth.uid() checks for the RLS policy. The * runtime's http_post_with_headers takes a method-agnostic body; we * use libcurl directly via a tiny shim. */ #include typedef struct { char *buf; size_t len; size_t cap; } _stub_resp_t; static size_t _stub_write(void *ptr, size_t size, size_t nmemb, void *u) { _stub_resp_t *r = (_stub_resp_t*)u; size_t add = size * nmemb; if (r->len + add + 1 > r->cap) { size_t nc = (r->cap ? r->cap : 4096); while (nc < r->len + add + 1) nc *= 2; char *nb = realloc(r->buf, nc); if (!nb) return 0; r->buf = nb; r->cap = nc; } memcpy(r->buf + r->len, ptr, add); r->len += add; r->buf[r->len] = '\0'; return add; } el_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey) { CURL *c = curl_easy_init(); if (!c) return EL_STR(""); char auth_hdr[2048]; snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", EL_CSTR(bearer_tok)); char api_hdr[2048]; snprintf(api_hdr, sizeof(api_hdr), "apikey: %s", EL_CSTR(apikey)); struct curl_slist *hdrs = NULL; hdrs = curl_slist_append(hdrs, auth_hdr); hdrs = curl_slist_append(hdrs, api_hdr); hdrs = curl_slist_append(hdrs, "Accept: application/json"); _stub_resp_t r = {0}; curl_easy_setopt(c, CURLOPT_URL, EL_CSTR(url)); curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs); curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(c, CURLOPT_TIMEOUT, 60L); curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, _stub_write); curl_easy_setopt(c, CURLOPT_WRITEDATA, &r); CURLcode rc = curl_easy_perform(c); long http_code = 0; curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &http_code); curl_easy_cleanup(c); curl_slist_free_all(hdrs); if (rc != CURLE_OK) { free(r.buf); return EL_STR(""); } if (http_code >= 400) { free(r.buf); return EL_STR(""); } if (!r.buf) return EL_STR(""); /* Note: small leak by design - response lives until process exits. * Mirrors the existing pattern in supabase_auth_user / gcs_get_token. */ return EL_STR(r.buf); } /* * supabase_upsert_user - POST a row to a Supabase table on behalf of a * logged-in user. Sends Authorization: Bearer + apikey: * + Prefer: resolution=merge-duplicates so RLS treats this as the user's * own write and PostgREST upserts on the (composite) primary key. * * `table_and_query` is the path after /rest/v1/, e.g. * "share_votes?on_conflict=share_id,user_id" * * Returns the response body (often "" with Prefer=return=minimal) or "" on * non-2xx / network failure. Callers should re-fetch aggregate state via a * separate GET to confirm the write landed. */ el_val_t supabase_upsert_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt, el_val_t table_and_query, el_val_t row_json) { char url[1024]; snprintf(url, sizeof(url), "%s/rest/v1/%s", EL_CSTR(project_url), EL_CSTR(table_and_query)); char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(user_jwt)); el_val_t headers = el_map_new(5, "Authorization", EL_STR(bearer), "apikey", anon_key, "Content-Type", EL_STR("application/json"), "Prefer", EL_STR("return=minimal,resolution=merge-duplicates"), "Accept", EL_STR("application/json")); return http_post_with_headers(EL_STR(url), row_json, headers); } /* * supabase_auth_user — verify a Supabase user JWT and return the user object. * Calls GET {project_url}/auth/v1/user with both apikey and Authorization headers. * Returns the raw user JSON, or "" on failure. */ el_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt) { char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(user_jwt)); char url[512]; snprintf(url, sizeof(url), "%s/auth/v1/user", EL_CSTR(project_url)); el_val_t headers = el_map_new(3, "Authorization", EL_STR(bearer), "apikey", anon_key, "Accept", EL_STR("application/json")); el_val_t resp = http_get_with_headers(EL_STR(url), headers); if (!resp) return EL_STR(""); return resp; } el_val_t cwd(void) { static char buf[4096]; if (!getcwd(buf, sizeof(buf))) buf[0] = '\0'; return EL_STR(buf); } el_val_t color_bold(el_val_t s) { return s; } /* unix_timestamp is now provided by el_runtime.c (back-compat shim). * Keeping the stub here would produce a duplicate symbol at link time. */ /* * supabase_insert — POST a JSON row to a Supabase table via the REST API. * Requires both apikey and Authorization: Bearer headers (Supabase convention). * Returns the response body, or "" on failure. */ el_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json) { char url[512]; snprintf(url, sizeof(url), "%s/rest/v1/%s", EL_CSTR(project_url), EL_CSTR(table)); char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(service_key)); el_val_t headers = el_map_new(4, "Authorization", EL_STR(bearer), "apikey", service_key, "Content-Type", EL_STR("application/json"), "Prefer", EL_STR("return=minimal,resolution=merge-duplicates")); return http_post_with_headers(EL_STR(url), row_json, headers); } /* * supabase_admin_invite — POST {project_url}/auth/v1/invite with the * service-role key to create-or-find a user and email them a magic-link * invitation. body_json should look like: * {"email":"...","data":{"name":"..."},"redirect_to":"..."} * Returns the JSON response (contains the new/existing user's id under "id"). * * Used by the Stripe webhook to auto-provision a Supabase account whenever * payment_intent.succeeded or setup_intent.succeeded fires - so every buyer * lands on /account with an account waiting for them, regardless of whether * they signed in before payment. */ el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json) { char url[512]; snprintf(url, sizeof(url), "%s/auth/v1/invite", EL_CSTR(project_url)); char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(service_key)); el_val_t headers = el_map_new(3, "Authorization", EL_STR(bearer), "apikey", service_key, "Content-Type", EL_STR("application/json")); return http_post_with_headers(EL_STR(url), body_json, headers); } /* * gcs_get_token — fetch an OAuth2 bearer token. * * Priority: * 1. GOOGLE_ACCESS_TOKEN env var (local dev: `gcloud auth print-access-token`) * 2. GCP metadata server (Cloud Run, automatic SA credentials) * * Returns the raw token string, or "" on failure. */ static el_val_t gcs_get_token(void) { /* 1. Env override (local dev) */ const char *env_tok = getenv("GOOGLE_ACCESS_TOKEN"); if (env_tok && *env_tok) return EL_STR(env_tok); /* 2. Metadata server */ el_val_t meta_url = EL_STR( "http://metadata.google.internal/computeMetadata/v1/instance" "/service-accounts/default/token"); el_val_t meta_headers = el_map_new(1, "Metadata-Flavor", EL_STR("Google")); el_val_t resp = http_get_with_headers(meta_url, meta_headers); if (!resp) return EL_STR(""); /* Parse access_token field from JSON response */ const char *s = EL_CSTR(resp); const char *key = "\"access_token\":\""; const char *p = strstr(s, key); if (!p) return EL_STR(""); p += strlen(key); const char *end = strchr(p, '"'); if (!end) return EL_STR(""); size_t len = (size_t)(end - p); char *tok = malloc(len + 1); memcpy(tok, p, len); tok[len] = '\0'; el_val_t result = EL_STR(tok); /* Note: intentional small leak — token lives for process lifetime */ return result; } /* * gcs_write — upload content to a GCS object. * * Uses the GCS JSON upload API (POST, media upload type). * Returns "ok" on success, "" on failure. */ el_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content) { el_val_t tok = gcs_get_token(); if (!tok || !*EL_CSTR(tok)) return EL_STR(""); /* URL-encode the object name (replace / with %2F) */ const char *obj = EL_CSTR(object_name); char encoded[1024]; size_t ei = 0; for (size_t i = 0; obj[i] && ei < sizeof(encoded) - 4; i++) { if (obj[i] == '/') { encoded[ei++] = '%'; encoded[ei++] = '2'; encoded[ei++] = 'F'; } else encoded[ei++] = obj[i]; } encoded[ei] = '\0'; /* Build upload URL */ char url[2048]; snprintf(url, sizeof(url), "https://storage.googleapis.com/upload/storage/v1/b/%s/o" "?uploadType=media&name=%s", EL_CSTR(bucket), encoded); /* POST with Authorization + Content-Type headers */ char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok)); el_val_t headers = el_map_new(2, "Authorization", EL_STR(bearer), "Content-Type", EL_STR("application/json")); el_val_t resp = http_post_with_headers(EL_STR(url), content, headers); if (!resp) return EL_STR(""); /* GCS returns the object metadata JSON on success */ const char *r = EL_CSTR(resp); if (strstr(r, "\"name\"")) return EL_STR("ok"); return EL_STR(""); } /* * gcs_read — download a GCS object's content. * * Returns the raw content, or "" if the object does not exist or on error. */ el_val_t gcs_read(el_val_t bucket, el_val_t object_name) { el_val_t tok = gcs_get_token(); if (!tok || !*EL_CSTR(tok)) return EL_STR(""); /* URL-encode object name */ const char *obj = EL_CSTR(object_name); char encoded[1024]; size_t ei = 0; for (size_t i = 0; obj[i] && ei < sizeof(encoded) - 4; i++) { if (obj[i] == '/') { encoded[ei++] = '%'; encoded[ei++] = '2'; encoded[ei++] = 'F'; } else encoded[ei++] = obj[i]; } encoded[ei] = '\0'; char url[2048]; snprintf(url, sizeof(url), "https://storage.googleapis.com/download/storage/v1/b/%s/o/%s?alt=media", EL_CSTR(bucket), encoded); char bearer[2048]; snprintf(bearer, sizeof(bearer), "Bearer %s", EL_CSTR(tok)); el_val_t headers = el_map_new(1, "Authorization", EL_STR(bearer)); el_val_t resp = http_get_with_headers(EL_STR(url), headers); if (!resp) return EL_STR(""); return resp; } /* Browser JS interop stubs — server-side no-ops for checkout.el browser globals. * checkout.el contains JS inline: window.neuronCheckoutFree&&window.neuronCheckoutFree() * which the El HTML parser exposes as C identifiers in the generated checkout.c. */ el_val_t window = 0; el_val_t neuronCheckoutFree(el_val_t v) { (void)v; return 0; }