Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dafa27c30c | |||
| 8dca40c288 | |||
| f0a6b55a13 | |||
| 843b6e07a7 | |||
| 0202b09d37 | |||
| f19403ba68 | |||
| 8d741fac20 | |||
| 28c47c11c9 | |||
| 8a8762ad4f | |||
| a936d2ebb7 | |||
| c49a838aad | |||
| 6075f49e8a |
@@ -81,7 +81,7 @@ jobs:
|
|||||||
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
|
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
|
||||||
"$DEST"
|
"$DEST"
|
||||||
ls -la "$DEST" | head -5
|
ls -la "$DEST" | head -5
|
||||||
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
|
echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Authenticate to GCP
|
- name: Authenticate to GCP
|
||||||
id: auth
|
id: auth
|
||||||
@@ -194,6 +194,7 @@ jobs:
|
|||||||
--image "$IMAGE" \
|
--image "$IMAGE" \
|
||||||
--region us-central1 \
|
--region us-central1 \
|
||||||
--project neuron-785695 \
|
--project neuron-785695 \
|
||||||
|
--execution-environment gen2 \
|
||||||
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
--service-account neuron-marketing-sa@neuron-785695.iam.gserviceaccount.com \
|
||||||
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
--update-env-vars "NODE_ENV=production,STRIPE_PUBLISHABLE_KEY=pk_test_51TPoHnJg9Fv1D3AUp1FEMcy4MGlKRZqs4scW66kjQFQjWofmNc2rottzXzDaXekHvuw1OQpyp2WCIsc7O5fXIG0G00HQQrkdGX,GCS_SHARE_BUCKET=neuron-shares-prod,SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9jb2pzZ2hhb25sdHVuaWRrenB3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc2NDIxNjgsImV4cCI6MjA5MzIxODE2OH0.e0FVFw1aahnrBVvnkR5R8a-RxCx095U8o_gsk7Quq3E,NEURON_LLM_0_FORMAT=anthropic,NEURON_LLM_0_MODEL=claude-sonnet-4-5,NEURON_LLM_0_URL=https://api.anthropic.com/v1/messages" \
|
||||||
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
--update-secrets "SUPABASE_SERVICE_KEY=supabase-service-key:latest,NEURON_LLM_0_KEY=anthropic-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,STRIPE_SECRET_KEY=stripe-secret-key-stage:latest,STRIPE_WEBHOOK_SECRET=stripe-webhook-secret-stage:latest,STRIPE_PRICE_PROFESSIONAL=stripe-price-professional-stage:latest,STRIPE_PRICE_FOUNDING=stripe-price-founding-stage:latest,STRIPE_PRICE_FAMILY_CHILD=stripe-price-family-child:latest,RESEND_API_KEY=resend-api-key:latest,DOCUSEAL_WEBHOOK_TOKEN=docuseal-webhook-token:latest" \
|
||||||
@@ -211,6 +212,7 @@ jobs:
|
|||||||
|
|
||||||
gcloud run services update marketing-stage \
|
gcloud run services update marketing-stage \
|
||||||
--region us-central1 --project neuron-785695 \
|
--region us-central1 --project neuron-785695 \
|
||||||
|
--execution-environment gen2 \
|
||||||
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
|
--update-env-vars "NEURON_ORIGIN=${STAGE_URL}" \
|
||||||
--quiet
|
--quiet
|
||||||
|
|
||||||
@@ -248,6 +250,7 @@ jobs:
|
|||||||
--image "$IMAGE" \
|
--image "$IMAGE" \
|
||||||
--region "$region" \
|
--region "$region" \
|
||||||
--project neuron-785695 \
|
--project neuron-785695 \
|
||||||
|
--execution-environment gen2 \
|
||||||
--quiet
|
--quiet
|
||||||
}
|
}
|
||||||
deploy us-central1 marketing-prod-us &
|
deploy us-central1 marketing-prod-us &
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Stage — Build, push & deploy to marketing-stage
|
|||||||
|
|
||||||
# Pipeline: build → push → deploy marketing-stage → smoke test.
|
# Pipeline: build → push → deploy marketing-stage → smoke test.
|
||||||
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
|
# STOPS HERE. No prod deploy. Merge to main when stage looks good.
|
||||||
|
# Triggered: 2026-05-05 (promote fix/gallery-layout-account-otp)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -78,7 +79,7 @@ jobs:
|
|||||||
git clone --depth 1 \
|
git clone --depth 1 \
|
||||||
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
|
"https://will:${CHECKOUT_TOKEN}@git.neuralplatform.ai/neuron-technologies/el.git" \
|
||||||
"$DEST"
|
"$DEST"
|
||||||
echo "EL_HOME=$DEST" >> "$GITHUB_ENV"
|
echo "EL_HOME=$DEST/lang" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Authenticate to GCP
|
- name: Authenticate to GCP
|
||||||
uses: google-github-actions/auth@v2
|
uses: google-github-actions/auth@v2
|
||||||
|
|||||||
@@ -29,3 +29,10 @@ src/assets/js/
|
|||||||
!dist/soul-demo.c
|
!dist/soul-demo.c
|
||||||
!dist/entrypoint.sh
|
!dist/entrypoint.sh
|
||||||
!dist/engram-snapshot.json
|
!dist/engram-snapshot.json
|
||||||
|
!dist/Dockerfile.soul-demo
|
||||||
|
!dist/k3s-soul-demo.yaml
|
||||||
|
|
||||||
|
# Build artifacts produced by the soul-demo packaging step in build-stage.sh
|
||||||
|
dist/soul-demo
|
||||||
|
dist/soul-demo-snapshot.json
|
||||||
|
dist/soul-demo-image.tar
|
||||||
|
|||||||
+27
-3
@@ -15,6 +15,7 @@ FROM debian:bookworm-slim AS builder
|
|||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --no-install-recommends \
|
||||||
build-essential \
|
build-essential \
|
||||||
|
curl \
|
||||||
libcurl4-openssl-dev \
|
libcurl4-openssl-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@@ -28,7 +29,10 @@ COPY runtime/el_runtime.c runtime/el_runtime.h ./
|
|||||||
# Pre-compile el_runtime as a separate cached layer.
|
# Pre-compile el_runtime as a separate cached layer.
|
||||||
# el_runtime.c changes rarely; main.c changes every run.
|
# 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.
|
# Splitting this out means el_runtime.o is cached across builds when only main.c changes.
|
||||||
RUN cc -O2 -c el_runtime.c -I. -o el_runtime.o
|
# -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
|
||||||
|
|
||||||
# ── Build neuron-web ──────────────────────────────────────────────────────────
|
# ── Build neuron-web ──────────────────────────────────────────────────────────
|
||||||
#
|
#
|
||||||
@@ -52,6 +56,10 @@ RUN cc -O2 -rdynamic \
|
|||||||
soul-demo.c vessel_stubs.c el_runtime.o \
|
soul-demo.c vessel_stubs.c el_runtime.o \
|
||||||
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
-lcurl -lpthread -ldl -lm -lssl -lcrypto
|
||||||
|
|
||||||
|
# ── Download k3s binary ───────────────────────────────────────────────────────
|
||||||
|
RUN curl -fL https://github.com/k3s-io/k3s/releases/download/v1.32.4%2Bk3s1/k3s -o /usr/local/bin/k3s \
|
||||||
|
&& chmod +x /usr/local/bin/k3s
|
||||||
|
|
||||||
# ── Stage 2: runtime image ────────────────────────────────────────────────────
|
# ── Stage 2: runtime image ────────────────────────────────────────────────────
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -64,11 +72,24 @@ RUN apt-get update \
|
|||||||
&& groupadd -r landing && useradd -r -g landing landing \
|
&& groupadd -r landing && useradd -r -g landing landing \
|
||||||
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
&& mkdir -p /srv/landing/assets /srv/landing/js /srv/landing/shares \
|
||||||
&& mkdir -p /srv/soul/engram-demo \
|
&& mkdir -p /srv/soul/engram-demo \
|
||||||
&& chown -R landing:landing /srv/landing /srv/soul
|
&& chown -R landing:landing /srv/landing /srv/soul \
|
||||||
|
&& mkdir -p /var/lib/rancher/k3s /tmp/k3s \
|
||||||
|
&& chown -R landing:landing /var/lib/rancher /tmp/k3s
|
||||||
|
|
||||||
COPY --from=builder /build/neuron-web /usr/local/bin/neuron-web
|
COPY --from=builder /build/neuron-web /usr/local/bin/neuron-web
|
||||||
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
|
COPY --from=builder /build/soul-demo /usr/local/bin/soul-demo
|
||||||
|
|
||||||
|
# k3s binary
|
||||||
|
COPY --from=builder /usr/local/bin/k3s /usr/local/bin/k3s
|
||||||
|
|
||||||
|
# soul-demo OCI image tar — k3s imports this at startup (no registry needed)
|
||||||
|
RUN mkdir -p /var/lib/rancher/k3s/agent/images
|
||||||
|
COPY dist/soul-demo-image.tar /var/lib/rancher/k3s/agent/images/soul-demo.tar
|
||||||
|
|
||||||
|
# k3s manifests — auto-applied when k3s starts
|
||||||
|
RUN mkdir -p /var/lib/rancher/k3s/server/manifests
|
||||||
|
COPY dist/k3s-soul-demo.yaml /var/lib/rancher/k3s/server/manifests/soul-demo.yaml
|
||||||
|
|
||||||
# Engram snapshot — baked in so soul has memory from cold start
|
# Engram snapshot — baked in so soul has memory from cold start
|
||||||
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
COPY dist/engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
||||||
|
|
||||||
@@ -91,8 +112,11 @@ ENV LANDING_ROOT=/srv/landing
|
|||||||
ENV PORT=8080
|
ENV PORT=8080
|
||||||
ENV NEURON_HOME=/srv/soul/engram-demo
|
ENV NEURON_HOME=/srv/soul/engram-demo
|
||||||
ENV NEURON_PORT=7772
|
ENV NEURON_PORT=7772
|
||||||
|
ENV K3S_DATA_DIR=/var/lib/rancher/k3s
|
||||||
|
ENV KUBECONFIG=/var/lib/rancher/k3s/server/cred/admin.kubeconfig
|
||||||
|
|
||||||
USER landing
|
# k3s requires root to create network namespaces and mount cgroups.
|
||||||
|
# Cloud Run gen2 sandbox is the security boundary here.
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["/usr/local/bin/entrypoint.sh"]
|
CMD ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|||||||
@@ -114,4 +114,22 @@ docker build \
|
|||||||
-t "marketing:${TAG}" \
|
-t "marketing:${TAG}" \
|
||||||
.
|
.
|
||||||
|
|
||||||
|
# Extract soul-demo binary and engram snapshot from built image
|
||||||
|
echo "==> Packaging soul-demo:local image for k3s..."
|
||||||
|
CONTAINER_ID=$(docker create "marketing:${TAG}")
|
||||||
|
docker cp "${CONTAINER_ID}:/usr/local/bin/soul-demo" dist/soul-demo
|
||||||
|
docker cp "${CONTAINER_ID}:/srv/soul/engram-demo/snapshot.json" dist/soul-demo-snapshot.json 2>/dev/null || true
|
||||||
|
docker rm "${CONTAINER_ID}"
|
||||||
|
|
||||||
|
# Build minimal soul-demo container image
|
||||||
|
cp dist/soul-demo-snapshot.json dist/engram-snapshot.json 2>/dev/null || true
|
||||||
|
docker build \
|
||||||
|
-f dist/Dockerfile.soul-demo \
|
||||||
|
-t soul-demo:local \
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Save as OCI tar for k3s to import at startup
|
||||||
|
docker save soul-demo:local -o dist/soul-demo-image.tar
|
||||||
|
echo "==> soul-demo:local image saved ($(du -sh dist/soul-demo-image.tar | cut -f1))"
|
||||||
|
|
||||||
echo "==> Done. marketing:${TAG} built."
|
echo "==> Done. marketing:${TAG} built."
|
||||||
|
|||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libcurl4 libssl3 ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& groupadd -r landing && useradd -r -g landing landing \
|
||||||
|
&& mkdir -p /srv/soul/engram-demo \
|
||||||
|
&& chown -R landing:landing /srv/soul
|
||||||
|
COPY soul-demo /usr/local/bin/soul-demo
|
||||||
|
COPY engram-snapshot.json /srv/soul/engram-demo/snapshot.json
|
||||||
|
ENV NEURON_HOME=/srv/soul/engram-demo
|
||||||
|
ENV NEURON_PORT=7772
|
||||||
|
USER landing
|
||||||
|
CMD ["/usr/local/bin/soul-demo"]
|
||||||
Vendored
+29
-15
@@ -1,21 +1,35 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/sh
|
||||||
# entrypoint.sh — Start soul-demo then neuron-web in the same container.
|
set -e
|
||||||
#
|
|
||||||
# soul-demo runs in the background on :7772 (localhost only, not exposed).
|
|
||||||
# neuron-web runs in the foreground on :8080 (Cloud Run health checks this).
|
|
||||||
# If neuron-web exits, the container exits. Soul crashing is non-fatal —
|
|
||||||
# chat will return "demo soul not responding" but the page stays up.
|
|
||||||
|
|
||||||
set -euo pipefail
|
echo "[entrypoint] Starting k3s server (embedded soul-demo orchestrator)..."
|
||||||
|
|
||||||
echo "[entrypoint] starting soul-demo on :7772"
|
# k3s server — single-node mode, disable unused components
|
||||||
/usr/local/bin/soul-demo &
|
# --disable traefik,servicelb: we don't need an ingress or LB
|
||||||
SOUL_PID=$!
|
# --disable metrics-server: saves ~50MB RAM
|
||||||
|
# --write-kubeconfig-mode=644: allow non-root reads
|
||||||
|
# --data-dir: use the pre-chowned dir
|
||||||
|
k3s server \
|
||||||
|
--disable traefik \
|
||||||
|
--disable servicelb \
|
||||||
|
--disable metrics-server \
|
||||||
|
--write-kubeconfig-mode=644 \
|
||||||
|
--data-dir /var/lib/rancher/k3s \
|
||||||
|
--node-name soul-node &
|
||||||
|
|
||||||
# Give the soul a few seconds to load its engram and seed safety nodes
|
K3S_PID=$!
|
||||||
sleep 4
|
|
||||||
|
|
||||||
echo "[entrypoint] soul-demo started (pid=$SOUL_PID)"
|
echo "[entrypoint] Waiting for k3s to become ready..."
|
||||||
echo "[entrypoint] starting neuron-web on :${PORT:-8080}"
|
until k3s kubectl get nodes --no-headers 2>/dev/null | grep -q "Ready"; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "[entrypoint] k3s ready. soul-demo Deployment will be applied automatically from manifests."
|
||||||
|
|
||||||
|
# Wait for soul-demo pod to be Running before starting neuron-web
|
||||||
|
echo "[entrypoint] Waiting for soul-demo pod..."
|
||||||
|
until k3s kubectl get pods -l app=soul-demo --no-headers 2>/dev/null | grep -q "Running"; do
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
echo "[entrypoint] soul-demo is running."
|
||||||
|
|
||||||
|
echo "[entrypoint] Starting neuron-web on port ${PORT:-8080}..."
|
||||||
exec /usr/local/bin/neuron-web
|
exec /usr/local/bin/neuron-web
|
||||||
|
|||||||
Vendored
+90
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: soul-demo
|
||||||
|
namespace: default
|
||||||
|
labels:
|
||||||
|
app: soul-demo
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: soul-demo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: soul-demo
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: soul-demo
|
||||||
|
image: soul-demo:local
|
||||||
|
imagePullPolicy: Never
|
||||||
|
ports:
|
||||||
|
- containerPort: 7772
|
||||||
|
env:
|
||||||
|
- name: NEURON_HOME
|
||||||
|
value: /srv/soul/engram-demo
|
||||||
|
- name: NEURON_PORT
|
||||||
|
value: "7772"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 1000m
|
||||||
|
memory: 512Mi
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 7772
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 15
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 7772
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
volumeMounts:
|
||||||
|
- name: engram-data
|
||||||
|
mountPath: /srv/soul/engram-demo
|
||||||
|
volumes:
|
||||||
|
- name: engram-data
|
||||||
|
emptyDir: {}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: soul-demo
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: soul-demo
|
||||||
|
ports:
|
||||||
|
- port: 7772
|
||||||
|
targetPort: 7772
|
||||||
|
nodePort: 7772
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: soul-demo
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: soul-demo
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 8
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
Vendored
+121
-122
File diff suppressed because one or more lines are too long
+79
-13
@@ -502,7 +502,7 @@ fn config_get(key: String) -> String {
|
|||||||
// function - it serves __html_file__ directly with text/html.
|
// function - it serves __html_file__ directly with text/html.
|
||||||
// This handler covers /api/* and /brand/* routes.
|
// 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__")
|
let src_dir: String = state_get("__src_dir__")
|
||||||
|
|
||||||
// ── Root — serve El-generated landing page ────────────────────────────────
|
// ── Root — serve El-generated landing page ────────────────────────────────
|
||||||
@@ -520,7 +520,7 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
|
|
||||||
// ── robots.txt ────────────────────────────────────────────────────────────
|
// ── robots.txt ────────────────────────────────────────────────────────────
|
||||||
if str_eq(path, "/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 ────────────────────────────────────────────────────────────
|
// ── About page ────────────────────────────────────────────────────────────
|
||||||
@@ -990,12 +990,18 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
let ua_safe: String = str_replace(str_replace(attest_ua, "\\", "\\\\"), "\"", "\\\"")
|
let ua_safe: String = str_replace(str_replace(attest_ua, "\\", "\\\\"), "\"", "\\\"")
|
||||||
// Write to Supabase waitlist (attestation in dedicated column)
|
// Write to Supabase waitlist (attestation in dedicated column)
|
||||||
waitlist_upsert(attest_email, attest_name, attest_plan, "founding-attestation", attest_text, attest_ua, 0)
|
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 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")
|
let attest_bucket: String = env("GCS_ATTEST_BUCKET")
|
||||||
if !str_eq(gcs_bucket, "") {
|
if str_eq(attest_bucket, "") {
|
||||||
let attest_key: String = "attestations/" + attest_ts + "-" + attest_email + ".json"
|
// Fall back to share bucket with attestations/ prefix for legacy deploys
|
||||||
let gcs_ok: String = gcs_write(gcs_bucket, attest_key, record)
|
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)
|
println("[attest] gcs write " + attest_key + " -> " + gcs_ok)
|
||||||
}
|
}
|
||||||
// Email notification
|
// Email notification
|
||||||
@@ -1117,15 +1123,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))
|
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).
|
// Turnstile: server-side verification is mandatory on every first
|
||||||
// Per-message verification breaks chat flow. Forms get full verification.
|
// 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")
|
let cf_token: String = json_get(body, "cf_token")
|
||||||
if !str_eq(cf_token, "") {
|
let ts_secret: String = state_get("__turnstile_secret_key__")
|
||||||
let ts_secret: String = "0x4AAAAAADHAZTok46L-l2sa9biSGpgN3GY"
|
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_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")
|
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.\"}"
|
return "{\"error\":\"Bot check failed. Please try again.\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1617,9 +1629,15 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
} else {
|
} else {
|
||||||
let share_html = fs_read(src_dir + "/shares/" + id + ".html")
|
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, "") {
|
if str_eq(share_html, "") {
|
||||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||||
}
|
}
|
||||||
|
if !str_starts_with(share_html, "<") {
|
||||||
|
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||||
|
}
|
||||||
return share_html
|
return share_html
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1827,6 +1845,52 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
|||||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
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 ───────────────────────────────────────────────────────────────────
|
// ── Startup ───────────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Order matters:
|
// Order matters:
|
||||||
@@ -1856,6 +1920,7 @@ let resend_api_key: String = env("RESEND_API_KEY")
|
|||||||
let supabase_anon_key: String = env("SUPABASE_ANON_KEY")
|
let supabase_anon_key: String = env("SUPABASE_ANON_KEY")
|
||||||
let supabase_service_key: String = env("SUPABASE_SERVICE_KEY")
|
let supabase_service_key: String = env("SUPABASE_SERVICE_KEY")
|
||||||
let supabase_project_url: String = "https://ocojsghaonltunidkzpw.supabase.co"
|
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.
|
// Origin — drives Stripe redirect URLs; never hardcoded to localhost.
|
||||||
let neuron_origin_env: String = env("NEURON_ORIGIN")
|
let neuron_origin_env: String = env("NEURON_ORIGIN")
|
||||||
@@ -1903,6 +1968,7 @@ state_set("__origin__", neuron_origin)
|
|||||||
state_set("__founding_sold_file__", sold_file)
|
state_set("__founding_sold_file__", sold_file)
|
||||||
state_set("__founding_sold__", int_to_str(real_sold))
|
state_set("__founding_sold__", int_to_str(real_sold))
|
||||||
state_set("__founding_total__", int_to_str(FOUNDING_TOTAL))
|
state_set("__founding_total__", int_to_str(FOUNDING_TOTAL))
|
||||||
|
state_set("__turnstile_secret_key__", turnstile_secret_key)
|
||||||
persist_founding_count(real_sold)
|
persist_founding_count(real_sold)
|
||||||
|
|
||||||
println(color_bold("Neuron") + " - " + neuron_origin)
|
println(color_bold("Neuron") + " - " + neuron_origin)
|
||||||
|
|||||||
+1
-1
@@ -1828,7 +1828,7 @@ fn page_open() -> String {
|
|||||||
button[disabled] { opacity: 0.6; cursor: not-allowed; }
|
button[disabled] { opacity: 0.6; cursor: not-allowed; }
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script src=\"https://cdn.jsdelivr.net/npm/marked/marked.min.js\"></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>
|
<script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>
|
||||||
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; }</style></noscript>
|
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; }</style></noscript>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user