2a3f998827
Dev — Build & local smoke test / build-smoke (pull_request) Failing after 13m24s
Add window/neuronCheckoutFree stubs to web_stubs.c — needed to link without undefined symbol errors on macOS (vessel stubs were already handled, web platform stubs were not). Add c_source directives to manifest.el so elb includes web_stubs.c and vessel_stubs.c in the link — requires the matching elb update in foundation/el PR #46. Move gallery_share_allowlist from module scope into gallery_page() to prevent elc from emitting a second main() for the gallery module, which caused a duplicate-symbol link error when combined with main.el. Update elc-linux-amd64 binary (rebuilt with RBrace fix from PR #46).
334 lines
14 KiB
C
334 lines
14 KiB
C
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#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 <curl/curl.h>
|
|
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 <user-jwt> + apikey: <anon>
|
|
* + 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; }
|