Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2447310367 | |||
| 2a3f998827 | |||
| 96fca7ebf7 | |||
| da669c67a1 | |||
| e3e6ec7ade | |||
| 5a8783ff0c | |||
| 032be3a058 | |||
| 6928a33685 | |||
| bb2be6398b | |||
| f7034c990a | |||
| 4ec5558517 | |||
| 0ace906823 | |||
| 067c83f8ff | |||
| 5f35ddde39 | |||
| e6d10fc3d5 | |||
| 7c4c0d9963 | |||
| aedb14f86c | |||
| c24b9b179b | |||
| 3e377e2bb6 | |||
| 9e77c3cbf0 | |||
| fef846e6f5 | |||
| 494f4ef585 | |||
| e68de7892f | |||
| 00e62bb010 | |||
| 1cf2ef8835 | |||
| f0a6b55a13 | |||
| 843b6e07a7 | |||
| 0202b09d37 | |||
| f19403ba68 | |||
| 8d741fac20 | |||
| 28c47c11c9 | |||
| 8a8762ad4f | |||
| a936d2ebb7 | |||
| c49a838aad | |||
| 6075f49e8a |
@@ -62,10 +62,6 @@ jobs:
|
||||
echo "=> Full build required"
|
||||
fi
|
||||
|
||||
- name: Set up El SDK
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Authenticate to GCP
|
||||
id: auth
|
||||
uses: google-github-actions/auth@v2
|
||||
@@ -102,12 +98,60 @@ jobs:
|
||||
# in the build context for Dockerfile COPY to succeed.
|
||||
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
||||
|
||||
- name: Build image (build-stage.sh)
|
||||
# ── El SDK setup ──────────────────────────────────────────────────────
|
||||
|
||||
- name: Extract El SDK from ci-base
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
env:
|
||||
EXTRACT_JS: '1'
|
||||
run: |
|
||||
./build-stage.sh "${{ steps.tag.outputs.tag }}"
|
||||
set -euo pipefail
|
||||
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest
|
||||
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:latest)
|
||||
sudo mkdir -p /opt/el
|
||||
docker cp "$CID:/opt/el" /opt/
|
||||
docker rm "$CID"
|
||||
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
|
||||
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
|
||||
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
|
||||
|
||||
# ── Build neuron-web binary ───────────────────────────────────────────
|
||||
|
||||
- name: Build neuron-web with elb
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
"$ELB" \
|
||||
--elc="$ELC" \
|
||||
--runtime="$EL_RUNTIME"
|
||||
echo "Binary: $(ls -lh dist/neuron-landing)"
|
||||
|
||||
# ── Compile JS client sources ─────────────────────────────────────────
|
||||
|
||||
- name: Compile JS El sources
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp "$EL_RUNTIME/el_runtime.js" src/js/
|
||||
mkdir -p dist/js
|
||||
for f in src/js/*.el; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .el)
|
||||
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
||||
echo " compiled: $f -> dist/js/${name}.js"
|
||||
done
|
||||
rm -f src/js/el_runtime.js
|
||||
|
||||
# ── Docker build + push ───────────────────────────────────────────────
|
||||
|
||||
- name: Build and tag image
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker build \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
|
||||
-f Dockerfile.stage \
|
||||
-t "marketing:${{ steps.tag.outputs.tag }}" \
|
||||
.
|
||||
docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}"
|
||||
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest"
|
||||
|
||||
|
||||
+106
-35
@@ -3,6 +3,15 @@ name: Dev — Build & local smoke test
|
||||
# Validates that the build compiles and the server starts cleanly.
|
||||
# No GCP deployment — this is the inner dev loop gate.
|
||||
# Merge to stage when you want a real environment.
|
||||
#
|
||||
# Build approach: pull ci-base from Artifact Registry (has elb + elc + runtime
|
||||
# at /opt/el), extract the SDK onto the runner host, then run elb build.
|
||||
# elb compiles each .el source independently — no combined mega-file, no OOM.
|
||||
# Output: dist/neuron-landing (linux/amd64). Dockerfile.stage COPYs it directly.
|
||||
#
|
||||
# For pull_request events: secrets are not injected, so ci-base can't be pulled.
|
||||
# Fall back to committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/.
|
||||
# No docker cache (no Artifact Registry auth), but the full build + smoke test runs.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,8 +20,20 @@ on:
|
||||
- 'src/**'
|
||||
- 'dist/**'
|
||||
- 'runtime/**'
|
||||
- 'Dockerfile.stage'
|
||||
- 'manifest.el'
|
||||
- 'Dockerfile.stage'
|
||||
- '.gitea/workflows/dev.yaml'
|
||||
- '.gitea/workflows/stage.yaml'
|
||||
- '.gitea/workflows/deploy.yaml'
|
||||
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'dist/**'
|
||||
- 'runtime/**'
|
||||
- 'manifest.el'
|
||||
- 'Dockerfile.stage'
|
||||
- '.gitea/workflows/dev.yaml'
|
||||
- '.gitea/workflows/stage.yaml'
|
||||
- '.gitea/workflows/deploy.yaml'
|
||||
@@ -34,63 +55,116 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up El SDK
|
||||
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
|
||||
# ── GCP auth (push/workflow_dispatch only) ────────────────────────────
|
||||
# pull_request events don't get secrets injected. GCP auth is skipped
|
||||
# for PRs — El SDK comes from committed bin/ + runtime/ instead.
|
||||
|
||||
- name: Authenticate to GCP
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
credentials_json: ${{ secrets.GCP_SA_KEY }}
|
||||
|
||||
- name: Set up gcloud SDK
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: google-github-actions/setup-gcloud@v2
|
||||
with:
|
||||
project_id: neuron-785695
|
||||
|
||||
- name: Configure docker auth for Artifact Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
|
||||
|
||||
# ── El SDK setup ──────────────────────────────────────────────────────
|
||||
# Push builds: extract elb + elc + runtime from ci-base (always latest).
|
||||
# PR builds: use committed bin/elb-linux-amd64 + bin/elc-linux-amd64 + runtime/.
|
||||
|
||||
- name: Extract El SDK from ci-base (push builds)
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev
|
||||
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:dev)
|
||||
sudo mkdir -p /opt/el
|
||||
docker cp "$CID:/opt/el" /opt/
|
||||
docker rm "$CID"
|
||||
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
|
||||
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
|
||||
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up El SDK from committed bin/ (PR builds)
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
DEST="${{ github.workspace }}/../foundation-el"
|
||||
mkdir -p "$DEST/dist/bin" "$DEST/dist/platform" "$DEST/el-compiler/runtime"
|
||||
cp bin/elb-linux-amd64 "$DEST/dist/bin/elb"
|
||||
cp bin/elc-linux-amd64 "$DEST/dist/platform/elc"
|
||||
chmod +x "$DEST/dist/bin/elb" "$DEST/dist/platform/elc"
|
||||
cp runtime/el_runtime.c "$DEST/el-compiler/runtime/"
|
||||
cp runtime/el_runtime.h "$DEST/el-compiler/runtime/"
|
||||
cp runtime/el_runtime.js "$DEST/el-compiler/runtime/"
|
||||
echo "ELB=$DEST/dist/bin/elb" >> "$GITHUB_ENV"
|
||||
echo "ELC=$DEST/dist/platform/elc" >> "$GITHUB_ENV"
|
||||
echo "EL_RUNTIME=$DEST/el-compiler/runtime" >> "$GITHUB_ENV"
|
||||
|
||||
# ── Build neuron-web binary ───────────────────────────────────────────
|
||||
|
||||
- name: Build neuron-web with elb
|
||||
run: |
|
||||
set -uo pipefail
|
||||
echo "ELB=$ELB ELC=$ELC EL_RUNTIME=$EL_RUNTIME"
|
||||
ls -la "$ELB" "$ELC"
|
||||
stdbuf -oL "$ELB" \
|
||||
--elc="$ELC" \
|
||||
--runtime="$EL_RUNTIME" 2>&1 | tee /tmp/elb.log
|
||||
ELB_EXIT=${PIPESTATUS[0]}
|
||||
if [ "$ELB_EXIT" -eq 0 ]; then
|
||||
echo "Binary: $(ls -lh dist/neuron-landing)"
|
||||
fi
|
||||
exit "$ELB_EXIT"
|
||||
|
||||
- name: Dump full elb output (on failure)
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== full elb output ==="
|
||||
cat /tmp/elb.log || echo "(no log file)"
|
||||
|
||||
# ── Compile JS client sources ─────────────────────────────────────────
|
||||
|
||||
- name: Compile JS El sources
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp "$EL_RUNTIME/el_runtime.js" src/js/
|
||||
mkdir -p dist/js
|
||||
for f in src/js/*.el; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .el)
|
||||
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
||||
echo " compiled: $f -> dist/js/${name}.js"
|
||||
done
|
||||
rm -f src/js/el_runtime.js
|
||||
|
||||
# ── Docker build + smoke test ─────────────────────────────────────────
|
||||
|
||||
- name: Compute image tag
|
||||
id: tag
|
||||
run: echo "tag=dev-${GITHUB_SHA:0:8}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Touch HTML placeholder files
|
||||
# El binary regenerates these at startup via fs_write. They must exist
|
||||
# in the build context for Dockerfile COPY to succeed. touch is
|
||||
# idempotent if the files already exist from a prior run.
|
||||
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
||||
|
||||
- name: Build El binary (elb)
|
||||
# elb compiles each .el source independently (no combined mega-file),
|
||||
# then links via cc. Output: dist/neuron-landing (linux/amd64 binary).
|
||||
# This avoids the exponential memory growth that hits elc on the
|
||||
# concatenated main-combined.el approach.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export EL_HOME=/opt/el
|
||||
/opt/el/dist/bin/elb build \
|
||||
--elc=/opt/el/dist/platform/elc \
|
||||
--runtime=/opt/el/el-compiler/runtime
|
||||
echo "Binary: $(ls -lh dist/neuron-landing)"
|
||||
|
||||
- name: Compile JS El sources
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ELC=/opt/el/dist/platform/elc
|
||||
mkdir -p dist/js
|
||||
for f in src/js/*.el; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .el)
|
||||
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
||||
echo " compiled: $f → dist/js/${name}.js"
|
||||
done
|
||||
|
||||
- name: Build Docker image (local only — no push)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${{ steps.tag.outputs.tag }}"
|
||||
CACHE_ARGS=""
|
||||
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||
CACHE_ARGS="--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest"
|
||||
fi
|
||||
docker build \
|
||||
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
$CACHE_ARGS \
|
||||
-f Dockerfile.stage \
|
||||
-t "marketing:${TAG}" \
|
||||
.
|
||||
@@ -107,8 +181,6 @@ jobs:
|
||||
-e LANDING_ROOT=/srv/landing \
|
||||
"$IMAGE"
|
||||
|
||||
# entrypoint.sh sleeps 4s for soul-demo to load before starting neuron-web.
|
||||
# Poll up to 45s total.
|
||||
for i in $(seq 1 15); do
|
||||
STATUS=$(curl -sSo /dev/null -w "%{http_code}" --max-time 5 http://localhost:8080/ || echo "000")
|
||||
echo "Attempt $i/15: HTTP $STATUS"
|
||||
@@ -125,4 +197,3 @@ jobs:
|
||||
docker stop dev-smoke && docker rm dev-smoke || true
|
||||
echo "Dev smoke test FAILED"
|
||||
exit 1
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ name: Stage — Build, push & deploy to marketing-stage
|
||||
|
||||
# Pipeline: build → push → deploy marketing-stage → smoke test.
|
||||
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
|
||||
# Triggered: 2026-05-05 (promote fix/gallery-layout-account-otp)
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -71,10 +72,6 @@ jobs:
|
||||
echo "=> Full build required"
|
||||
fi
|
||||
|
||||
- name: Set up El SDK
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: echo "EL_HOME=/opt/el" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Authenticate to GCP
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
@@ -103,10 +100,60 @@ jobs:
|
||||
# in the build context for Dockerfile COPY to succeed.
|
||||
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
||||
|
||||
- name: Build image (build-stage.sh)
|
||||
# ── El SDK setup ──────────────────────────────────────────────────────
|
||||
|
||||
- name: Extract El SDK from ci-base
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
./build-stage.sh "${{ steps.tag.outputs.tag }}"
|
||||
set -euo pipefail
|
||||
docker pull us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage
|
||||
CID=$(docker create us-central1-docker.pkg.dev/neuron-785695/neuron-ci/ci-base:stage)
|
||||
sudo mkdir -p /opt/el
|
||||
docker cp "$CID:/opt/el" /opt/
|
||||
docker rm "$CID"
|
||||
echo "ELB=/opt/el/dist/bin/elb" >> "$GITHUB_ENV"
|
||||
echo "ELC=/opt/el/dist/platform/elc" >> "$GITHUB_ENV"
|
||||
echo "EL_RUNTIME=/opt/el/el-compiler/runtime" >> "$GITHUB_ENV"
|
||||
|
||||
# ── Build neuron-web binary ───────────────────────────────────────────
|
||||
|
||||
- name: Build neuron-web with elb
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
"$ELB" \
|
||||
--elc="$ELC" \
|
||||
--runtime="$EL_RUNTIME"
|
||||
echo "Binary: $(ls -lh dist/neuron-landing)"
|
||||
|
||||
# ── Compile JS client sources ─────────────────────────────────────────
|
||||
|
||||
- name: Compile JS El sources
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp "$EL_RUNTIME/el_runtime.js" src/js/
|
||||
mkdir -p dist/js
|
||||
for f in src/js/*.el; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .el)
|
||||
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
||||
echo " compiled: $f -> dist/js/${name}.js"
|
||||
done
|
||||
rm -f src/js/el_runtime.js
|
||||
|
||||
# ── Docker build + push ───────────────────────────────────────────────
|
||||
|
||||
- name: Build and tag image
|
||||
if: steps.changetype.outputs.asset_only != 'true'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker build \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest \
|
||||
-f Dockerfile.stage \
|
||||
-t "marketing:${{ steps.tag.outputs.tag }}" \
|
||||
.
|
||||
docker tag "marketing:${{ steps.tag.outputs.tag }}" "${{ steps.tag.outputs.image }}"
|
||||
docker tag "marketing:${{ steps.tag.outputs.tag }}" "us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:stage-latest"
|
||||
|
||||
|
||||
+8
-1
@@ -25,7 +25,14 @@ RUN apt-get update \
|
||||
WORKDIR /build
|
||||
|
||||
COPY runtime/el_runtime.c runtime/el_runtime.h ./
|
||||
RUN cc -O2 -c el_runtime.c -I. -o el_runtime.o
|
||||
|
||||
# Pre-compile el_runtime as a separate cached layer.
|
||||
# el_runtime.c changes rarely; main.c changes every run.
|
||||
# Splitting this out means el_runtime.o is cached across builds when only main.c changes.
|
||||
# -DHAVE_CURL: the staged el_runtime.c (from el.git) guards the OTLP observability
|
||||
# section (emit_metric, emit_log, trace_span_*) behind #ifdef HAVE_CURL.
|
||||
# libcurl IS installed above, so define HAVE_CURL to enable those functions.
|
||||
RUN cc -O2 -DHAVE_CURL -c el_runtime.c -I. -o el_runtime.o
|
||||
|
||||
COPY dist/soul-demo.c dist/vessel_stubs.c ./
|
||||
|
||||
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
+15
-86
@@ -2,111 +2,40 @@
|
||||
#
|
||||
# build-stage.sh — Build the Stage marketing image (neuron-web + soul-demo).
|
||||
#
|
||||
# Pipeline:
|
||||
# 1. Stage the foundation El runtime into ./runtime/.
|
||||
# 2. Compile client-side El sources (src/js/*.el) to dist/js/*.js using
|
||||
# the JS-capable elc binary at bin/elc-linux-amd64 (CI) or the local
|
||||
# elc (dev). Output is gitignored and rebuilt every run.
|
||||
# 3. Concatenate src/*.el into dist/main-combined.el (component-first,
|
||||
# main.el last; matches the historical order from build-local.sh).
|
||||
# 4. Compile dist/main-combined.el → dist/main.c using the canonical
|
||||
# native elc at foundation/el/dist/platform/elc.
|
||||
# 5. Inject the host-side stub forward declarations into dist/main.c
|
||||
# (sed header rewrite, same set as the prior in-Dockerfile sed).
|
||||
# 6. docker buildx build --platform linux/amd64 -f Dockerfile.stage.
|
||||
#
|
||||
# bootstrap.py is no longer in the build path. The container image now
|
||||
# expects dist/main.c to be a finished C source — it just runs cc on it.
|
||||
# Thin wrapper around elb. The El build system handles compilation.
|
||||
# ELB, ELC, and EL_RUNTIME must be set by the caller (extracted from ci-base
|
||||
# in CI, or pointed at the local El SDK in dev).
|
||||
#
|
||||
# Usage:
|
||||
# ./build-stage.sh <tag> — build marketing:<tag>
|
||||
# ELB=/opt/el/dist/bin/elb ELC=... EL_RUNTIME=... ./build-stage.sh <tag>
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TAG="${1:-dev}"
|
||||
|
||||
LANDING_DIR=$(pwd)
|
||||
EL_HOME="${EL_HOME:-${LANDING_DIR}/../../foundation/el}"
|
||||
ELC="${EL_HOME}/dist/platform/elc"
|
||||
RUNTIME_SRC="${EL_HOME}/el-compiler/runtime"
|
||||
|
||||
# JS-capable elc: prefer committed bin/elc-linux-amd64 on CI (linux/amd64),
|
||||
# fall back to the local elc from the El checkout on macOS dev.
|
||||
if [ -f "${LANDING_DIR}/bin/elc-linux-amd64" ] && uname -m | grep -q x86_64; then
|
||||
ELC_JS="${LANDING_DIR}/bin/elc-linux-amd64"
|
||||
elif [ -x "${ELC}" ]; then
|
||||
ELC_JS="${ELC}"
|
||||
else
|
||||
echo "elc for JS compilation not found — expected bin/elc-linux-amd64 or ${ELC}" >&2
|
||||
if [ -z "${ELB:-}" ] || [ -z "${ELC:-}" ] || [ -z "${EL_RUNTIME:-}" ]; then
|
||||
echo "Error: ELB, ELC, and EL_RUNTIME must be set" >&2
|
||||
echo " Extract from ci-base or point to local El SDK at foundation/el" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "${ELC}" ]; then
|
||||
echo "elc not found at ${ELC}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Staging El runtime from ${RUNTIME_SRC}"
|
||||
mkdir -p runtime dist
|
||||
cp "${RUNTIME_SRC}/el_runtime.c" runtime/
|
||||
cp "${RUNTIME_SRC}/el_runtime.h" runtime/
|
||||
|
||||
# The JS compiler looks for el_runtime.js in the same directory as the
|
||||
# source file being compiled. Copy it there so --bundle can inline it.
|
||||
cp "${RUNTIME_SRC}/el_runtime.js" "${LANDING_DIR}/src/js/"
|
||||
echo "==> Building neuron-web with elb"
|
||||
"$ELB" build --elc "$ELC" --runtime "$EL_RUNTIME"
|
||||
echo " Binary: $(ls -lh dist/neuron-landing)"
|
||||
|
||||
echo "==> Compiling client-side El (src/js/*.el) → dist/js/"
|
||||
cp "$EL_RUNTIME/el_runtime.js" src/js/
|
||||
mkdir -p dist/js
|
||||
for f in "${LANDING_DIR}/src/js/"*.el; do
|
||||
for f in src/js/*.el; do
|
||||
[ -f "$f" ] || continue
|
||||
name=$(basename "$f" .el)
|
||||
"${ELC_JS}" --target=js --bundle --minify --obfuscate "$f" > "${LANDING_DIR}/dist/js/${name}.js"
|
||||
"$ELC" --target=js --bundle --minify --obfuscate "$f" > "dist/js/${name}.js"
|
||||
echo " compiled: src/js/${name}.el → dist/js/${name}.js"
|
||||
done
|
||||
|
||||
# Clean up the staged runtime (not a source file)
|
||||
rm -f "${LANDING_DIR}/src/js/el_runtime.js"
|
||||
|
||||
echo "==> Combining El sources → dist/main-combined.el"
|
||||
COMPONENTS=(nav hero pillars how_it_works inference efficiency comparison
|
||||
environmental enterprise mission local_first pricing marketplace viral
|
||||
footer styles about founding_badge terms enterprise_terms checkout safety
|
||||
gallery account)
|
||||
{
|
||||
for f in "${COMPONENTS[@]}"; do
|
||||
if [ -f "src/${f}.el" ]; then
|
||||
grep -hv '^[[:space:]]*from\|^[[:space:]]*import' "src/${f}.el"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
grep -v '^from\|^import' src/main.el
|
||||
} > dist/main-combined.el
|
||||
echo " $(wc -l < dist/main-combined.el) lines"
|
||||
|
||||
echo "==> Compiling dist/main-combined.el → dist/main.c via ${ELC}"
|
||||
"${ELC}" dist/main-combined.el > dist/main.c
|
||||
echo " $(wc -l < dist/main.c) lines of C"
|
||||
|
||||
echo "==> Injecting host-side stub forward declarations"
|
||||
# GNU vs BSD sed: -i with no arg works on GNU, breaks on macOS BSD sed
|
||||
# (BSD requires -i ''). Detect and branch.
|
||||
SED_INPLACE=(-i)
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
SED_INPLACE=(-i)
|
||||
else
|
||||
SED_INPLACE=(-i '')
|
||||
fi
|
||||
sed "${SED_INPLACE[@]}" \
|
||||
's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_post_auth_json(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t http_delete_auth(el_val_t url, el_val_t bearer_tok, el_val_t apikey);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);\nel_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_val_t body_json);\nel_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);|' \
|
||||
dist/main.c
|
||||
rm -f src/js/el_runtime.js
|
||||
|
||||
echo "==> Building Docker image marketing:${TAG}"
|
||||
# Plain `docker build` — the gitea runner doesn't ship buildx, so
|
||||
# `docker buildx build --platform ...` exits 125 ("unknown flag:
|
||||
# --platform"). The runner host is already linux/amd64 so the
|
||||
# explicit --platform is redundant. BUILDKIT_INLINE_CACHE works with
|
||||
# plain docker as long as DOCKER_BUILDKIT=1 is set (default on the
|
||||
# runner).
|
||||
docker build \
|
||||
--build-arg BUILDKIT_INLINE_CACHE=1 \
|
||||
--cache-from us-central1-docker.pkg.dev/neuron-785695/neuron-marketing/marketing:latest \
|
||||
|
||||
Vendored
+121
-122
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -325,3 +325,9 @@ el_val_t gcs_read(el_val_t bucket, el_val_t object_name) {
|
||||
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; }
|
||||
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
package "neuron-landing" {
|
||||
version "1.0.0"
|
||||
description "Neuron marketing landing page server"
|
||||
authors ["Will Anderson <will@neurontechnologies.ai>"]
|
||||
authors ["Will Anderson <will.anderson@neurontechnologies.ai>"]
|
||||
edition "2026"
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ build {
|
||||
target "release"
|
||||
entry "src/main.el"
|
||||
output "dist/"
|
||||
c_source "dist/web_stubs.c"
|
||||
c_source "dist/vessel_stubs.c"
|
||||
}
|
||||
|
||||
+1325
-66
File diff suppressed because it is too large
Load Diff
+134
-7
@@ -22,6 +22,9 @@
|
||||
* EL_STR(s) cast string literal to el_val_t
|
||||
* EL_CSTR(v) cast el_val_t back to const char*
|
||||
* EL_INT(v) identity — el_val_t is already int64_t
|
||||
* EL_NULL null / zero value
|
||||
* EL_FALSE boolean false (0)
|
||||
* EL_TRUE boolean true (1)
|
||||
*
|
||||
* Link requirements:
|
||||
* -lcurl — required for the HTTP client (http_get, http_post, llm_*).
|
||||
@@ -53,6 +56,8 @@ typedef int64_t el_val_t;
|
||||
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
||||
#define EL_INT(v) (v)
|
||||
#define EL_NULL ((el_val_t)0)
|
||||
#define EL_FALSE ((el_val_t)0)
|
||||
#define EL_TRUE ((el_val_t)1)
|
||||
|
||||
/* Float values share the el_val_t (int64) slot via a bit-cast.
|
||||
* The codegen emits Float literals as `el_from_float(<dbl>)` so the
|
||||
@@ -76,8 +81,8 @@ extern "C" {
|
||||
|
||||
/* ── I/O ──────────────────────────────────────────────────────────────────── */
|
||||
|
||||
void println(el_val_t s);
|
||||
void print(el_val_t s);
|
||||
el_val_t println(el_val_t s);
|
||||
el_val_t print(el_val_t s);
|
||||
el_val_t readline(void);
|
||||
|
||||
/* ── String builtins ─────────────────────────────────────────────────────── */
|
||||
@@ -90,6 +95,7 @@ el_val_t str_len(el_val_t s);
|
||||
el_val_t str_concat(el_val_t a, el_val_t b);
|
||||
el_val_t int_to_str(el_val_t n);
|
||||
el_val_t str_to_int(el_val_t s);
|
||||
el_val_t native_str_to_int(el_val_t s);
|
||||
el_val_t str_slice(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t str_contains(el_val_t s, el_val_t sub);
|
||||
el_val_t str_replace(el_val_t s, el_val_t from, el_val_t to);
|
||||
@@ -117,6 +123,10 @@ el_val_t el_min(el_val_t a, el_val_t b);
|
||||
void el_retain(el_val_t v);
|
||||
void el_release(el_val_t v);
|
||||
|
||||
/* ── Scoped arena (CLI use) ───────────────────────────────────────────────── */
|
||||
el_val_t el_arena_push(void);
|
||||
el_val_t el_arena_pop(el_val_t mark);
|
||||
|
||||
/* ── List ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t el_list_new(el_val_t count, ...);
|
||||
@@ -140,10 +150,11 @@ el_val_t http_post(el_val_t url, el_val_t body);
|
||||
el_val_t http_post_json(el_val_t url, el_val_t json_body);
|
||||
el_val_t http_get_with_headers(el_val_t url, el_val_t headers_map);
|
||||
el_val_t http_post_with_headers(el_val_t url, el_val_t body, el_val_t headers_map);
|
||||
el_val_t http_post_json_with_headers(el_val_t url, el_val_t headers_map, el_val_t json_body);
|
||||
el_val_t http_post_form_auth(el_val_t url, el_val_t form_body, el_val_t auth_header);
|
||||
el_val_t http_delete(el_val_t url);
|
||||
void http_serve(el_val_t port, el_val_t handler);
|
||||
void http_set_handler(el_val_t name);
|
||||
el_val_t http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler(el_val_t name);
|
||||
|
||||
/* HTTP server v2 ─────────────────────────────────────────────────────────────
|
||||
* Same dispatch model as http_serve, but the handler signature is widened:
|
||||
@@ -164,8 +175,8 @@ void http_set_handler(el_val_t name);
|
||||
* The 3-arg http_serve(port, handler) remains supported unchanged for
|
||||
* existing handlers (e.g. products/web/server.el): it dispatches with
|
||||
* (method, path, body), hardcodes 200 OK, and auto-detects content type. */
|
||||
void http_serve_v2(el_val_t port, el_val_t handler);
|
||||
void http_set_handler_v2(el_val_t name);
|
||||
el_val_t http_serve_v2(el_val_t port, el_val_t handler);
|
||||
el_val_t http_set_handler_v2(el_val_t name);
|
||||
|
||||
/* Build an HTTP response envelope. `headers_json` should be a JSON object
|
||||
* literal like `{"WWW-Authenticate":"Basic"}` (or "" / "{}" for none). The
|
||||
@@ -176,6 +187,11 @@ void http_set_handler_v2(el_val_t name);
|
||||
* auto-content-type contract for legacy handlers that return plain bodies. */
|
||||
el_val_t http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
|
||||
/* SSE connection fd — set by http_worker_v2 before calling the El handler,
|
||||
* cleared afterwards. Defined in el_seed.c; called from el_runtime.c.
|
||||
* The getter is exposed as __http_conn_fd() to El programs. */
|
||||
void el_seed_set_http_conn_fd(int fd);
|
||||
|
||||
/* HTTP timeout — every libcurl request honors EL_HTTP_TIMEOUT_MS (default
|
||||
* 60000ms). Read lazily on first use, so setting the env var any time before
|
||||
* the first http_* call is sufficient. */
|
||||
@@ -211,12 +227,15 @@ el_val_t url_decode(el_val_t s); /* '+' → space, %XX → byte */
|
||||
* {"p":[],"a":["href","title"],"strong":[],...}
|
||||
* where each value is the array of attribute names allowed for that tag. */
|
||||
el_val_t el_html_sanitize(el_val_t input_html, el_val_t allowlist_json);
|
||||
el_val_t html_raw(el_val_t s);
|
||||
el_val_t html_escape(el_val_t s);
|
||||
|
||||
/* ── Filesystem ──────────────────────────────────────────────────────────── */
|
||||
|
||||
el_val_t fs_read(el_val_t path);
|
||||
el_val_t fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t fs_list(el_val_t path);
|
||||
el_val_t fs_list_json(el_val_t path);
|
||||
el_val_t fs_exists(el_val_t path);
|
||||
el_val_t fs_mkdir(el_val_t path); /* mkdir -p, mode 0755 */
|
||||
|
||||
@@ -246,6 +265,9 @@ el_val_t json_set(el_val_t json_str, el_val_t key, el_val_t value);
|
||||
el_val_t json_array_len(el_val_t json_str);
|
||||
el_val_t json_array_get(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_array_get_string(el_val_t json_str, el_val_t index);
|
||||
el_val_t json_escape_string(el_val_t sv);
|
||||
el_val_t json_build_object(el_val_t kvs);
|
||||
el_val_t json_build_array(el_val_t items);
|
||||
|
||||
/* ── Time ────────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -258,6 +280,7 @@ el_val_t time_to_parts(el_val_t ts);
|
||||
el_val_t time_from_parts(el_val_t secs, el_val_t ns, el_val_t tz);
|
||||
el_val_t time_add(el_val_t ts, el_val_t n, el_val_t unit);
|
||||
el_val_t time_diff(el_val_t ts1, el_val_t ts2, el_val_t unit);
|
||||
el_val_t now_ns(void);
|
||||
|
||||
/* ── Instant + Duration: first-class temporal types ──────────────────────────
|
||||
* Both types share the el_val_t (int64) slot. Instants are nanoseconds
|
||||
@@ -414,6 +437,8 @@ el_val_t state_set(el_val_t key, el_val_t value);
|
||||
el_val_t state_get(el_val_t key);
|
||||
el_val_t state_del(el_val_t key);
|
||||
el_val_t state_keys(void);
|
||||
el_val_t state_has(el_val_t key);
|
||||
el_val_t state_get_or(el_val_t key, el_val_t default_val);
|
||||
|
||||
/* ── Float formatting ────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -505,9 +530,15 @@ el_val_t parse_int(el_val_t s, el_val_t default_val);
|
||||
|
||||
/* ── Process ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
void exit_program(el_val_t code);
|
||||
el_val_t exit_program(el_val_t code);
|
||||
el_val_t getpid_now(void);
|
||||
|
||||
/* Self-terminating memory guard. Reads ELC_MAX_MEM_MB (default 512) and
|
||||
* exits with code 1 if resident memory exceeds the limit. Call periodically
|
||||
* during long compilation loops (e.g. after each function is compiled).
|
||||
* Returns 0 when memory is within bounds. */
|
||||
el_val_t el_mem_check(void);
|
||||
|
||||
/* ── CGI identity ─────────────────────────────────────────────────────────────
|
||||
* Called at the start of main() in CGI programs (those with a `cgi {}` block).
|
||||
* Records the program's DHARMA identity before any other code executes. */
|
||||
@@ -745,12 +776,108 @@ el_val_t exec_capture(el_val_t cmd); /* run shell command, capture stdout */
|
||||
el_val_t exec(el_val_t cmd); /* exec(cmd) → stdout String (30s timeout) */
|
||||
el_val_t exec_bg(el_val_t cmd); /* exec_bg(cmd) → PID String (non-blocking) */
|
||||
|
||||
/* ── Stdout redirection (used by compiler JS pipeline) ───────────────────── */
|
||||
el_val_t stdout_to_file(el_val_t path); /* redirect process stdout to a file */
|
||||
el_val_t stdout_restore(void); /* restore process stdout to terminal */
|
||||
|
||||
el_val_t emit_log(el_val_t level, el_val_t msg, el_val_t fields_json);
|
||||
el_val_t emit_metric(el_val_t name, el_val_t value, el_val_t tags_json);
|
||||
el_val_t trace_span_start(el_val_t name);
|
||||
el_val_t trace_span_end(el_val_t span_handle);
|
||||
el_val_t emit_event(el_val_t name, el_val_t duration_ms);
|
||||
|
||||
el_val_t __thread_create(el_val_t fn_name_v, el_val_t arg_v);
|
||||
el_val_t __thread_join(el_val_t tid_v);
|
||||
|
||||
/* ── __ prefixed aliases (self-hosting compiler ABI) ─────────────────────────
|
||||
* The El self-hosting compiler emits calls to __-prefixed names. These are
|
||||
* forwarding wrappers around the existing el_runtime functions above. */
|
||||
|
||||
/* I/O */
|
||||
el_val_t __println(el_val_t s);
|
||||
el_val_t __print(el_val_t s);
|
||||
el_val_t __readline(void);
|
||||
|
||||
/* String */
|
||||
el_val_t __int_to_str(el_val_t n);
|
||||
el_val_t __str_to_int(el_val_t s);
|
||||
el_val_t __float_to_str(el_val_t f);
|
||||
el_val_t __str_to_float(el_val_t s);
|
||||
el_val_t __str_len(el_val_t s);
|
||||
el_val_t __str_char_at(el_val_t s, el_val_t i);
|
||||
el_val_t __str_cmp(el_val_t a, el_val_t b);
|
||||
el_val_t __str_ncmp(el_val_t a, el_val_t b, el_val_t n);
|
||||
el_val_t __str_concat_raw(el_val_t a, el_val_t b);
|
||||
el_val_t __str_slice_raw(el_val_t s, el_val_t start, el_val_t end);
|
||||
el_val_t __str_alloc(el_val_t n);
|
||||
el_val_t __str_set_char(el_val_t s, el_val_t i, el_val_t c);
|
||||
|
||||
/* URL encoding */
|
||||
el_val_t __url_encode(el_val_t s);
|
||||
el_val_t __url_decode(el_val_t s);
|
||||
|
||||
/* Environment */
|
||||
el_val_t __env_get(el_val_t key);
|
||||
|
||||
/* Subprocess */
|
||||
el_val_t __exec(el_val_t cmd);
|
||||
el_val_t __exec_bg(el_val_t cmd);
|
||||
|
||||
/* Process */
|
||||
el_val_t __exit_program(el_val_t code);
|
||||
|
||||
/* Filesystem */
|
||||
el_val_t __fs_exists(el_val_t path);
|
||||
el_val_t __fs_mkdir(el_val_t path);
|
||||
el_val_t __fs_read(el_val_t path);
|
||||
el_val_t __fs_write(el_val_t path, el_val_t content);
|
||||
el_val_t __fs_write_bytes(el_val_t path, el_val_t bytes, el_val_t n);
|
||||
el_val_t __fs_list_raw(el_val_t path);
|
||||
|
||||
/* HTTP server */
|
||||
el_val_t __http_response(el_val_t status, el_val_t headers_json, el_val_t body);
|
||||
el_val_t __http_serve(el_val_t port, el_val_t handler);
|
||||
el_val_t __http_serve_v2(el_val_t port, el_val_t handler);
|
||||
|
||||
/* HTTP conn fd / SSE (weak; overridden by el_seed.c when linked together) */
|
||||
el_val_t __http_conn_fd(void);
|
||||
el_val_t __http_sse_open(el_val_t conn_id);
|
||||
el_val_t __http_sse_send(el_val_t conn_id, el_val_t data);
|
||||
el_val_t __http_sse_close(el_val_t conn_id);
|
||||
|
||||
/* HTTP client (requires HAVE_CURL; stubs provided for no-curl builds) */
|
||||
el_val_t __http_do(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_map, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t timeout_ms);
|
||||
el_val_t __http_do_map_to_file(el_val_t method, el_val_t url, el_val_t body,
|
||||
el_val_t headers_json, el_val_t output_path);
|
||||
|
||||
/* JSON */
|
||||
el_val_t __json_array_get(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_get_string(el_val_t json, el_val_t index);
|
||||
el_val_t __json_array_len(el_val_t json);
|
||||
el_val_t __json_get(el_val_t json, el_val_t key);
|
||||
el_val_t __json_get_raw(el_val_t json, el_val_t key);
|
||||
el_val_t __json_set(el_val_t json, el_val_t key, el_val_t value);
|
||||
el_val_t __json_parse_map(el_val_t json_str);
|
||||
el_val_t __json_stringify_val(el_val_t val);
|
||||
|
||||
/* Hashing */
|
||||
el_val_t __sha256_hex(el_val_t s);
|
||||
|
||||
/* State K/V */
|
||||
el_val_t __state_del(el_val_t key);
|
||||
el_val_t __state_get(el_val_t key);
|
||||
el_val_t __state_keys(void);
|
||||
el_val_t __state_set(el_val_t key, el_val_t val);
|
||||
|
||||
/* UUID */
|
||||
el_val_t __uuid_v4(void);
|
||||
|
||||
/* Args */
|
||||
el_val_t __args_json(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+37
-41
@@ -11,6 +11,17 @@
|
||||
// 4. User fills name, email, card - submits
|
||||
// 5. stripe.confirmPayment() → redirects to /marketplace/success
|
||||
|
||||
fn checkout_nav_html() -> String {
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo" aria-label="Neuron home"><img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="28"></a>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
|
||||
fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
let is_founding: Bool = str_eq(plan, "founding")
|
||||
let is_free: Bool = str_eq(plan, "free")
|
||||
@@ -53,17 +64,9 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
<li>2 devices included</li>"
|
||||
} }
|
||||
|
||||
return
|
||||
<nav id="nav">
|
||||
<div class="nav-inner">
|
||||
<a href="/" class="nav-logo" aria-label="Neuron home"><img src="/assets/brand/neuron-wordmark-on-light.png" srcset="/assets/brand/neuron-wordmark-on-light@2x.png 2x" alt="Neuron" height="28"></a>
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
let nav_html: String = checkout_nav_html()
|
||||
|
||||
<main style="min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;">
|
||||
let main_html: String = <main style="min-height: 100vh; padding: clamp(6rem, 14vh, 9rem) 2rem 4rem;">
|
||||
<div class="checkout-shell">
|
||||
|
||||
<!-- Left: order summary -->
|
||||
@@ -94,14 +97,14 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
<div class="checkout-form-wrap">
|
||||
|
||||
<!-- Auth section: visible immediately for free, collapsed (optional) for paid plans -->
|
||||
<div id="auth-section" {#if is_free}{#else}style="display:none;"{/if}>
|
||||
{#if is_free}
|
||||
<p class="label" style="margin-bottom: 1.5rem; color: var(--navy);">Create your account.</p>
|
||||
<p class="checkout-auth-hint" style="margin-bottom: 2rem;">No charge today. Add your card to reserve your spot - you won't be billed until you upgrade.</p>
|
||||
{#else}
|
||||
<p class="label" style="margin-bottom: 1.25rem;">Sign in (optional)</p>
|
||||
<p class="checkout-auth-hint">Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.</p>
|
||||
{/if}
|
||||
<div id=\"auth-section\" " + (if is_free { "" } else { "style=\"display:none;\"" }) + ">
|
||||
" + (if is_free { "
|
||||
<p class=\"label\" style=\"margin-bottom: 1.5rem; color: var(--navy);\">Create your account.</p>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-bottom: 2rem;\">No card required. Your account is free, forever.</p>
|
||||
" } else { "
|
||||
<p class=\"label\" style=\"margin-bottom: 1.25rem;\">Sign in (optional)</p>
|
||||
<p class=\"checkout-auth-hint\">Sign in to link this purchase to an existing account. Or skip and create one later - we'll match it to your email.</p>
|
||||
" }) + "
|
||||
|
||||
<div class="checkout-social-btns">
|
||||
<button type="button" class="checkout-social-btn" id="btn-google" onclick="signInWith('google')">
|
||||
@@ -135,6 +138,16 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Free-tier success panel: shown after account creation, no card needed -->
|
||||
" + (if is_free { "
|
||||
<div id=\"free-success\" style=\"display:none; text-align:center; padding: 2.5rem 1rem;\">
|
||||
<div style=\"font-size:2.5rem; margin-bottom:1.25rem;\">✓</div>
|
||||
<p class=\"label\" style=\"margin-bottom:.75rem; color:var(--navy);\">You're in.</p>
|
||||
<p class=\"checkout-auth-hint\" style=\"margin-bottom:2rem;\">Your free account is ready. Download Neuron to get started.</p>
|
||||
<a href=\"/marketplace\" class=\"checkout-submit\" style=\"display:inline-block; text-decoration:none; padding:.875rem 2rem;\">Go to your account →</a>
|
||||
</div>
|
||||
" } else { "" }) + "
|
||||
|
||||
<!-- Payment form (visible immediately - no auth wall) -->
|
||||
<div id="payment-section" {#if is_free}style="display:none;"{/if}>
|
||||
<div id="auth-badge" style="display:none; margin-bottom: 1.5rem;"></div>
|
||||
@@ -226,23 +239,14 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Supabase JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js"></script>
|
||||
<!-- Stripe.js -->
|
||||
<script src="https://js.stripe.com/v3/" async></script>
|
||||
let style_html: String = checkout_style_html()
|
||||
|
||||
<style>
|
||||
.checkout-shell {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.checkout-shell { grid-template-columns: 1fr; gap: 3rem; }
|
||||
let free_init_script: String = if is_free { "<script>document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});</script>" } else { "" }
|
||||
return nav_html + main_html + "<!-- Supabase JS --><script src=\"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js\"></script><!-- Stripe.js --><script src=\"https://js.stripe.com/v3/\" async></script>" + style_html + "<script src=\"/js/checkout-auth.js\" defer></script><script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan=\"{plan}\";window.NEURON_CFG.pub_key=\"{pub_key}\";</script><script src=\"/js/checkout-stripe.js\" defer></script>" + free_init_script
|
||||
}
|
||||
|
||||
fn checkout_style_html() -> String {
|
||||
<style>
|
||||
.checkout-plan-name {
|
||||
font-family: var(--head);
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
@@ -484,12 +488,4 @@ fn checkout_page(plan: String, pub_key: String) -> String {
|
||||
}
|
||||
.checkout-auth-badge strong { color: var(--navy); font-weight: 500; }
|
||||
</style>
|
||||
|
||||
<script src="/js/checkout-auth.js" defer></script>
|
||||
|
||||
<script>window.NEURON_CFG=window.NEURON_CFG||{};window.NEURON_CFG.plan="{plan}";window.NEURON_CFG.pub_key="{pub_key}";</script><script src="/js/checkout-stripe.js" defer></script>
|
||||
|
||||
{#if is_free}
|
||||
<script>document.addEventListener('DOMContentLoaded',function(){window.neuronCheckoutFree&&window.neuronCheckoutFree()});</script>
|
||||
{/if}
|
||||
}
|
||||
|
||||
+4
-6
@@ -166,9 +166,7 @@ fn enterprise() -> String {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
<style>
|
||||
.ent-inquiry-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -179,7 +177,7 @@ fn enterprise() -> String {
|
||||
.ent-inquiry-form > div[style*="grid-column:1/-1"],
|
||||
.ent-inquiry-form > div[style*="grid-column: 1 / -1"] { grid-column: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="/js/enterprise.js" defer></script>
|
||||
</style>
|
||||
<script src="/js/enterprise.js" defer></script>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ fn environmental() -> String {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<style>
|
||||
@media (max-width: 768px) {
|
||||
.env-grid { grid-template-columns: 1fr !important; gap: 2rem !important; }
|
||||
}
|
||||
</style>
|
||||
</section>
|
||||
}
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@
|
||||
// anchors, which the HTML5 parser resolves via the adoption agency algorithm,
|
||||
// producing mismatched </div> tags that break gallery-grid's closing tag and
|
||||
// pull sibling elements into the grid as spurious grid items.
|
||||
let gallery_share_allowlist: String = "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[]}"
|
||||
|
||||
fn gallery_page(cards_json: String, supabase_url: String, supabase_anon_key: String) -> String {
|
||||
// Moved from module-level to avoid duplicate main() when linked with other modules.
|
||||
let gallery_share_allowlist: String = "{\"p\":[],\"br\":[],\"strong\":[],\"em\":[],\"u\":[],\"s\":[],\"code\":[],\"pre\":[],\"ul\":[],\"ol\":[],\"li\":[],\"h1\":[],\"h2\":[],\"h3\":[],\"h4\":[],\"blockquote\":[]}"
|
||||
let i: Int = 0
|
||||
let cards_html: String = ""
|
||||
let n: Int = json_array_len(cards_json)
|
||||
|
||||
+53
-3
@@ -76,17 +76,59 @@ fn main() -> Void {
|
||||
saveSession(session);
|
||||
}
|
||||
var msgCount = session.count || 0;
|
||||
var _headerResetInterval = null;
|
||||
|
||||
function _nextMidnightUTC() {
|
||||
var now = new Date();
|
||||
var midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
||||
return Math.floor(midnight.getTime() / 1000);
|
||||
}
|
||||
|
||||
function _startHeaderResetTimer() {
|
||||
var el = document.getElementById('neuron-demo-countdown');
|
||||
if (!el) return;
|
||||
if (_headerResetInterval) return; // already ticking
|
||||
var resetAt = _nextMidnightUTC();
|
||||
function _tick() {
|
||||
var secsLeft = Math.max(0, resetAt - Math.floor(Date.now() / 1000));
|
||||
var hh = Math.floor(secsLeft / 3600);
|
||||
var mm = Math.floor((secsLeft % 3600) / 60);
|
||||
var ss = secsLeft % 60;
|
||||
var pad = function(n) { return n < 10 ? '0' + n : '' + n; };
|
||||
var ts = hh + ':' + pad(mm) + ':' + pad(ss);
|
||||
var el2 = document.getElementById('neuron-demo-countdown');
|
||||
if (!el2) { clearInterval(_headerResetInterval); _headerResetInterval = null; return; }
|
||||
el2.textContent = 'resets in ' + ts;
|
||||
el2.style.color = 'rgba(255,255,255,0.65)';
|
||||
el2.style.fontWeight = '600';
|
||||
if (secsLeft <= 0) {
|
||||
clearInterval(_headerResetInterval);
|
||||
_headerResetInterval = null;
|
||||
el2.textContent = '10 questions left';
|
||||
el2.style.color = '#ffffff';
|
||||
el2.style.fontWeight = '700';
|
||||
}
|
||||
}
|
||||
_tick();
|
||||
_headerResetInterval = setInterval(_tick, 1000);
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
var el = document.getElementById('neuron-demo-countdown');
|
||||
if (!el) return;
|
||||
var remaining = MAX - msgCount;
|
||||
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
|
||||
el.style.color = '#ffffff';
|
||||
el.style.fontWeight = '700';
|
||||
if (remaining <= 0) {
|
||||
_startHeaderResetTimer();
|
||||
} else {
|
||||
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
|
||||
el.textContent = remaining + ' question' + (remaining === 1 ? '' : 's') + ' left';
|
||||
el.style.color = '#ffffff';
|
||||
el.style.fontWeight = '700';
|
||||
}
|
||||
}
|
||||
|
||||
window.neuronDemoReset = function() {
|
||||
if (_headerResetInterval) { clearInterval(_headerResetInterval); _headerResetInterval = null; }
|
||||
clearSession();
|
||||
session = { messages: [], count: 0, context: '' };
|
||||
msgCount = 0;
|
||||
@@ -236,6 +278,14 @@ fn main() -> Void {
|
||||
if (!input || btn.disabled) return;
|
||||
var msg = input.value.trim();
|
||||
if (!msg) return;
|
||||
var MAX_CHARS = 8000;
|
||||
if (msg.length > MAX_CHARS) {
|
||||
if (input) {
|
||||
input.style.outline = '2px solid #e53e3e';
|
||||
setTimeout(function() { input.style.outline = ''; }, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
input.value = '';
|
||||
btn.disabled = true;
|
||||
addMsg('user', msg);
|
||||
|
||||
+10
-5
@@ -33,8 +33,11 @@ fn main() -> Void {
|
||||
if (user && user.id) { window._neuronSupaId = user.id; }
|
||||
var auth = document.getElementById('auth-section');
|
||||
if (auth) auth.style.display = 'none';
|
||||
var payment = document.getElementById('payment-section');
|
||||
if (payment) payment.style.display = '';
|
||||
var isFree = (window.NEURON_CFG || {}).plan === 'free';
|
||||
if (!isFree) {
|
||||
var payment = document.getElementById('payment-section');
|
||||
if (payment) payment.style.display = '';
|
||||
}
|
||||
|
||||
if (user) {
|
||||
var badge = document.getElementById('auth-badge');
|
||||
@@ -55,9 +58,11 @@ fn main() -> Void {
|
||||
if (emailEl) emailEl.value = user.email;
|
||||
}
|
||||
|
||||
var userEmail = user ? (user.email || '') : '';
|
||||
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
||||
if (typeof initStripe === 'function') initStripe(userEmail, userName);
|
||||
if (!isFree) {
|
||||
var userEmail = user ? (user.email || '') : '';
|
||||
var userName = user ? ((user.user_metadata && user.user_metadata.full_name) || '') : '';
|
||||
if (typeof window.initStripe === 'function') window.initStripe(userEmail, userName);
|
||||
}
|
||||
}
|
||||
|
||||
function checkExistingSession() {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// checkout-free.el -- Free plan: reveal payment section after auth completes.
|
||||
// Watches the auth-badge element; when it becomes visible, shows payment-section.
|
||||
// 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
|
||||
|
||||
fn main() -> Void {
|
||||
native_js("(function() {
|
||||
var pay = document.getElementById('payment-section');
|
||||
if (!pay) return;
|
||||
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) {
|
||||
pay.style.display = '';
|
||||
if (auth) auth.style.display = 'none';
|
||||
success.style.display = '';
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
@@ -31,8 +31,13 @@ fn main() -> Void {
|
||||
if (spinner) spinner.style.display = loading ? '' : 'none';
|
||||
}
|
||||
|
||||
// Free plan has no payment form — bail out entirely.
|
||||
if (str_eq(PLAN, 'free')) return;
|
||||
|
||||
window._neuronMode = 'payment';
|
||||
var paymentEl = null;
|
||||
var userEmail = '';
|
||||
var userName = '';
|
||||
|
||||
function appearance() {
|
||||
return {
|
||||
@@ -80,7 +85,7 @@ fn main() -> Void {
|
||||
return fetch('/api/payment-intent', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ plan: PLAN, timing: timing })
|
||||
body: JSON.stringify({ plan: PLAN, timing: timing, email: userEmail, name: userName })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
@@ -117,11 +122,17 @@ fn main() -> Void {
|
||||
});
|
||||
}
|
||||
|
||||
fetchAndMount();
|
||||
var tNow = document.getElementById('timing-now');
|
||||
var tLater = document.getElementById('timing-later');
|
||||
if (tNow) tNow.addEventListener('change', fetchAndMount);
|
||||
if (tLater) tLater.addEventListener('change', fetchAndMount);
|
||||
// Don't init Stripe at page load — wait for auth.
|
||||
// checkout-auth.el calls window.initStripe(email, name) after sign-in.
|
||||
window.initStripe = function(email, name) {
|
||||
userEmail = email || '';
|
||||
userName = name || '';
|
||||
fetchAndMount();
|
||||
var tNow = document.getElementById('timing-now');
|
||||
var tLater = document.getElementById('timing-later');
|
||||
if (tNow) tNow.addEventListener('change', fetchAndMount);
|
||||
if (tLater) tLater.addEventListener('change', fetchAndMount);
|
||||
};
|
||||
|
||||
var form = document.getElementById('payment-form');
|
||||
if (form) form.addEventListener('submit', async function(e) {
|
||||
|
||||
+108
-29
@@ -485,7 +485,7 @@ fn config_get(key: String) -> String {
|
||||
// function - it serves __html_file__ directly with text/html.
|
||||
// This handler covers /api/* and /brand/* routes.
|
||||
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
fn handle_request_inner(method: String, path: String, body: String) -> String {
|
||||
let src_dir: String = state_get("__src_dir__")
|
||||
|
||||
// ── Root — serve El-generated landing page ────────────────────────────────
|
||||
@@ -503,7 +503,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
|
||||
// ── robots.txt ────────────────────────────────────────────────────────────
|
||||
if str_eq(path, "/robots.txt") {
|
||||
return "User-agent: *\nAllow: /\nSitemap: https://neurontechnologies.ai/sitemap.xml\n"
|
||||
return "User-agent: *\nAllow: /\n"
|
||||
}
|
||||
|
||||
// ── About page ────────────────────────────────────────────────────────────
|
||||
@@ -567,23 +567,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
let timing: String = json_get_string(body, "timing")
|
||||
if str_eq(timing, "") { let timing = "now" }
|
||||
// Free tier: SetupIntent — save card details without charging.
|
||||
// Card is stored on a Stripe Customer; billing begins only if the
|
||||
// user later upgrades to a paid plan.
|
||||
// Free tier: no card required. Return immediately — no Stripe interaction.
|
||||
if str_eq(plan, "free") {
|
||||
let si_body: String = "automatic_payment_methods[enabled]=true"
|
||||
+ "&usage=off_session"
|
||||
+ "&metadata[plan]=free"
|
||||
let auth_header: String = "Bearer " + stripe_key
|
||||
let si_resp: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/setup_intents",
|
||||
si_body,
|
||||
auth_header)
|
||||
if str_starts_with(si_resp, "{") {
|
||||
let inner: String = str_slice(si_resp, 1, str_len(si_resp))
|
||||
return "{\"setup_mode\":true,\"plan\":\"free\"," + inner
|
||||
}
|
||||
return si_resp
|
||||
return "{\"plan\":\"free\",\"free\":true,\"no_payment_required\":true}"
|
||||
}
|
||||
// Hard cap: block founding checkouts when 1,000 spots are filled
|
||||
if str_eq(plan, "founding") {
|
||||
@@ -595,6 +581,27 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
let auth_header: String = "Bearer " + stripe_key
|
||||
|
||||
// Find-or-create Stripe Customer by email upfront so every intent
|
||||
// is attached to an existing customer — prevents duplicate customers.
|
||||
let pi_email: String = json_get_string(body, "email")
|
||||
let pi_name: String = json_get_string(body, "name")
|
||||
let pi_cus_id: String = ""
|
||||
if !str_eq(pi_email, "") {
|
||||
let pi_email_enc: String = str_replace(str_replace(pi_email, "@", "%40"), "+", "%2B")
|
||||
let pi_search_url: String = "https://api.stripe.com/v1/customers/search?query=email%3A%22" + pi_email_enc + "%22&limit=1"
|
||||
let pi_search: String = http_get_auth(pi_search_url, auth_header)
|
||||
let pi_cus_id = json_get_string(pi_search, "id")
|
||||
if str_eq(pi_cus_id, "") {
|
||||
let pi_name_enc: String = str_replace(pi_name, " ", "%20")
|
||||
let pi_cus_body: String = "email=" + pi_email_enc
|
||||
+ "&name=" + pi_name_enc
|
||||
+ "&metadata[plan]=" + plan
|
||||
+ "&metadata[source]=neuron-checkout"
|
||||
let pi_cus_resp: String = http_post_form_auth("https://api.stripe.com/v1/customers", pi_cus_body, auth_header)
|
||||
let pi_cus_id = json_get_string(pi_cus_resp, "id")
|
||||
}
|
||||
}
|
||||
|
||||
// Setup-mode path: save payment method, do not charge. Only valid
|
||||
// for Professional (Founding is one-shot lifetime, charges immediately).
|
||||
if str_eq(plan, "professional") && str_eq(timing, "later") {
|
||||
@@ -603,6 +610,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
+ "&metadata[plan]=" + plan
|
||||
+ "&metadata[hold_until]=launch"
|
||||
+ "&metadata[launch_target]=2026-09-01"
|
||||
let si_body = if !str_eq(pi_cus_id, "") { si_body + "&customer=" + pi_cus_id } else { si_body }
|
||||
let si_resp: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/setup_intents",
|
||||
si_body,
|
||||
@@ -625,6 +633,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
+ "&automatic_payment_methods[enabled]=true"
|
||||
+ "&metadata[plan]=" + plan
|
||||
+ "&metadata[timing]=" + timing
|
||||
let pi_body = if !str_eq(pi_cus_id, "") { pi_body + "&customer=" + pi_cus_id } else { pi_body }
|
||||
let response: String = http_post_form_auth(
|
||||
"https://api.stripe.com/v1/payment_intents",
|
||||
pi_body,
|
||||
@@ -973,12 +982,18 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let ua_safe: String = str_replace(str_replace(attest_ua, "\\", "\\\\"), "\"", "\\\"")
|
||||
// Write to Supabase waitlist (attestation in dedicated column)
|
||||
waitlist_upsert(attest_email, attest_name, attest_plan, "founding-attestation", attest_text, attest_ua, 0)
|
||||
// Also save to GCS as immutable legal record
|
||||
// Also save to GCS as immutable legal record.
|
||||
// Written to the dedicated attestations bucket (GCS_ATTEST_BUCKET) which
|
||||
// is private and separate from the public-read shares bucket.
|
||||
let record: String = "{\"plan\":\"" + attest_plan + "\",\"name\":\"" + n_safe + "\",\"email\":\"" + e_safe + "\",\"timestamp\":\"" + attest_ts + "\",\"attestation\":\"" + t_safe + "\",\"user_agent\":\"" + ua_safe + "\"}"
|
||||
let gcs_bucket: String = env("GCS_SHARE_BUCKET")
|
||||
if !str_eq(gcs_bucket, "") {
|
||||
let attest_key: String = "attestations/" + attest_ts + "-" + attest_email + ".json"
|
||||
let gcs_ok: String = gcs_write(gcs_bucket, attest_key, record)
|
||||
let attest_bucket: String = env("GCS_ATTEST_BUCKET")
|
||||
if str_eq(attest_bucket, "") {
|
||||
// Fall back to share bucket with attestations/ prefix for legacy deploys
|
||||
let attest_bucket = env("GCS_SHARE_BUCKET")
|
||||
}
|
||||
if !str_eq(attest_bucket, "") {
|
||||
let attest_key: String = attest_ts + "-" + attest_email + ".json"
|
||||
let gcs_ok: String = gcs_write(attest_bucket, attest_key, record)
|
||||
println("[attest] gcs write " + attest_key + " -> " + gcs_ok)
|
||||
}
|
||||
// Email notification
|
||||
@@ -1069,6 +1084,10 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(msg, "") {
|
||||
return "{\"error\":\"message required\"}"
|
||||
}
|
||||
// Input length guard: ~2000 tokens ≈ 8000 characters
|
||||
if str_len(msg) > 8000 {
|
||||
return "{\"error\":\"Message too long. Please keep your message under 8000 characters.\"}"
|
||||
}
|
||||
// Rate limit: 10 chats per uid per day (UTC day, keyed by uid).
|
||||
// State key: "__rl_<uid>" → "<count>|<day_number>"
|
||||
// day_number = unix_timestamp / 86400 (integer UTC day)
|
||||
@@ -1100,15 +1119,21 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
}
|
||||
state_set(rl_key, int_to_str(rl_count + 1) + "|" + int_to_str(today_day))
|
||||
}
|
||||
// Turnstile: verify on first message only (tokens are single-use).
|
||||
// Per-message verification breaks chat flow. Forms get full verification.
|
||||
// Turnstile: server-side verification is mandatory on every first
|
||||
// message (tokens are single-use; per-message verification would
|
||||
// break streaming chat flow so only the first message carries one).
|
||||
// Requests without a cf_token are rejected outright — the widget
|
||||
// must execute successfully before the first POST is sent.
|
||||
let cf_token: String = json_get(body, "cf_token")
|
||||
if !str_eq(cf_token, "") {
|
||||
let ts_secret: String = "0x4AAAAAADHAZTok46L-l2sa9biSGpgN3GY"
|
||||
let ts_secret: String = state_get("__turnstile_secret_key__")
|
||||
if str_eq(cf_token, "") && !str_eq(ts_secret, "") {
|
||||
return "{\"error\":\"Bot check required. Please complete the challenge.\"}"
|
||||
}
|
||||
if !str_eq(cf_token, "") && !str_eq(ts_secret, "") {
|
||||
let verify_body: String = "secret=" + ts_secret + "&response=" + cf_token
|
||||
let verify_resp: String = http_post("https://challenges.cloudflare.com/turnstile/v0/siteverify", verify_body)
|
||||
let verify_resp: String = http_post("https://challenges.cloudflare.com/turnstile/v1/siteverify", verify_body)
|
||||
let is_valid: String = json_get(verify_resp, "success")
|
||||
if str_eq(is_valid, "false") {
|
||||
if !str_eq(is_valid, "true") {
|
||||
return "{\"error\":\"Bot check failed. Please try again.\"}"
|
||||
}
|
||||
}
|
||||
@@ -1600,9 +1625,15 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
} else {
|
||||
let share_html = fs_read(src_dir + "/shares/" + id + ".html")
|
||||
}
|
||||
// Guard against empty responses and GCS error JSON (e.g. {"error":...}).
|
||||
// A valid share card always starts with "<" (HTML). Anything else is
|
||||
// treated as a missing card to avoid leaking bucket names or GCS details.
|
||||
if str_eq(share_html, "") {
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
if !str_starts_with(share_html, "<") {
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
return share_html
|
||||
}
|
||||
|
||||
@@ -1810,6 +1841,52 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
|
||||
// ── Security header wrapper ───────────────────────────────────────────────────
|
||||
//
|
||||
// Injects mandatory security headers on every response. Called by
|
||||
// handle_request which is the actual http_set_handler target; the inner
|
||||
// dispatcher (handle_request_inner) returns plain bodies so all the existing
|
||||
// route code is unchanged.
|
||||
//
|
||||
// Headers applied:
|
||||
// Strict-Transport-Security — forces HTTPS for 2 years + preload
|
||||
// X-Content-Type-Options — no MIME sniffing
|
||||
// X-Frame-Options — no framing except same origin
|
||||
// Referrer-Policy — full URL within origin, origin-only cross-site
|
||||
// Permissions-Policy — deny geo/mic/camera
|
||||
// Content-Security-Policy — allow self + trusted CDNs used by the app
|
||||
|
||||
fn sec_headers_json() -> String {
|
||||
"{\"Strict-Transport-Security\":\"max-age=63072000; includeSubDomains; preload\","
|
||||
+ "\"X-Content-Type-Options\":\"nosniff\","
|
||||
+ "\"X-Frame-Options\":\"SAMEORIGIN\","
|
||||
+ "\"Referrer-Policy\":\"strict-origin-when-cross-origin\","
|
||||
+ "\"Permissions-Policy\":\"geolocation=(), microphone=(), camera=()\","
|
||||
+ "\"Content-Security-Policy\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.jsdelivr.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; frame-src https://challenges.cloudflare.com; connect-src 'self' https://api.stripe.com https://*.supabase.co; img-src 'self' data: https:; font-src 'self' data:\"}"
|
||||
}
|
||||
|
||||
fn handle_request(method: String, path: String, body: String) -> String {
|
||||
let inner_resp: String = handle_request_inner(method, path, body)
|
||||
// Detect envelope already set by inner handler (starts with
|
||||
// {"el_http_response":1). If so, let it pass through unmodified —
|
||||
// the status code it carries takes precedence and we must not
|
||||
// double-wrap. (Currently inner never returns an envelope, but guard
|
||||
// defensively so a future route returning http_response(...) still works.)
|
||||
if str_starts_with(inner_resp, "{\"el_http_response\":1") {
|
||||
return inner_resp
|
||||
}
|
||||
// Detect the __status__ convention used by many routes so we can forward
|
||||
// the correct HTTP status code while still injecting security headers.
|
||||
let status_code: Int = 200
|
||||
if str_starts_with(inner_resp, "{\"__status__\":") {
|
||||
let status_str: String = json_get(inner_resp, "__status__")
|
||||
if !str_eq(status_str, "") {
|
||||
let status_code = str_to_int(status_str)
|
||||
}
|
||||
}
|
||||
http_response(status_code, sec_headers_json(), inner_resp)
|
||||
}
|
||||
|
||||
// ── Startup ───────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Order matters:
|
||||
@@ -1839,6 +1916,7 @@ let resend_api_key: String = env("RESEND_API_KEY")
|
||||
let supabase_anon_key: String = env("SUPABASE_ANON_KEY")
|
||||
let supabase_service_key: String = env("SUPABASE_SERVICE_KEY")
|
||||
let supabase_project_url: String = "https://ocojsghaonltunidkzpw.supabase.co"
|
||||
let turnstile_secret_key: String = env("TURNSTILE_SECRET_KEY")
|
||||
|
||||
// Origin — drives Stripe redirect URLs; never hardcoded to localhost.
|
||||
let neuron_origin_env: String = env("NEURON_ORIGIN")
|
||||
@@ -1886,6 +1964,7 @@ state_set("__origin__", neuron_origin)
|
||||
state_set("__founding_sold_file__", sold_file)
|
||||
state_set("__founding_sold__", int_to_str(real_sold))
|
||||
state_set("__founding_total__", int_to_str(FOUNDING_TOTAL))
|
||||
state_set("__turnstile_secret_key__", turnstile_secret_key)
|
||||
persist_founding_count(real_sold)
|
||||
|
||||
println(color_bold("Neuron") + " - " + neuron_origin)
|
||||
|
||||
+2
-2
@@ -133,8 +133,6 @@ fn marketplace() -> String {
|
||||
|
||||
<script src="/js/marketplace.js" defer></script>
|
||||
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#marketplace { padding: 6rem 0; }
|
||||
.marketplace-header { margin-bottom: 4rem; }
|
||||
@@ -229,4 +227,6 @@ fn marketplace() -> String {
|
||||
}
|
||||
.dev-textarea { resize: vertical; min-height: 120px; }
|
||||
</style>
|
||||
|
||||
</section>
|
||||
}
|
||||
|
||||
+1
-2
@@ -49,7 +49,6 @@ fn nav() -> String {
|
||||
<a href="/#pricing" class="nav-mobile-cta">Get Access</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/nav.js" defer></script>
|
||||
</nav>
|
||||
|
||||
<script src="/js/nav.js" defer></script>
|
||||
}
|
||||
|
||||
+7
-6
@@ -1828,8 +1828,8 @@ fn page_open() -> String {
|
||||
button[disabled] { opacity: 0.6; cursor: not-allowed; }
|
||||
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\" integrity=\"sha384-948ahk4ZmxYVYOc+rxN1H2gM1EJ2Duhp7uHtZ4WSLkV4Vtx5MUqnV+l7u9B+jFv+\" crossorigin=\"anonymous\"></script>
|
||||
<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>
|
||||
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; }</style></noscript>
|
||||
|
||||
<!-- Google Analytics -->
|
||||
@@ -1966,7 +1966,8 @@ fn page_open() -> String {
|
||||
}
|
||||
|
||||
fn page_close() -> String {
|
||||
return <script src="/js/chat-widget.js" defer></script>
|
||||
let widgets: String = <div id="page-widgets">
|
||||
<script src="/js/chat-widget.js" defer></script>
|
||||
|
||||
<!-- ── Neuron Demo Chat Widget ──────────────────────────────────────────────── -->
|
||||
<div id="neuron-demo-btn">
|
||||
@@ -1997,7 +1998,7 @@ fn page_close() -> String {
|
||||
<div id="neuron-demo-messages"></div>
|
||||
<div id="neuron-demo-turnstile" style="padding:0.75rem 1rem 0;transition:opacity 0.6s,max-height 0.6s;overflow:hidden;max-height:80px"></div>
|
||||
<div id="neuron-demo-input-row" style="display:none">
|
||||
<input type="text" id="neuron-demo-text" placeholder="Ask me anything..." autocomplete="off">
|
||||
<input type="text" id="neuron-demo-text" placeholder="Ask me anything..." autocomplete="off" maxlength="8000">
|
||||
<button id="neuron-demo-send" onclick="neuronDemoSend()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2023,6 +2024,6 @@ fn page_close() -> String {
|
||||
</div>
|
||||
|
||||
<script src="/js/styles.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
</div>
|
||||
return widgets + "</body></html>"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user