Compare commits

..

1 Commits

Author SHA1 Message Date
will.anderson 4989641b39 legal: reconcile Terms with locked 18+ and never-auto-contact decisions; add Privacy Policy
The published Terms directly contradicted two locked safety decisions and the
shipping app copy, and the in-app Privacy Policy link 404'd. Both are legal
exposure under CA SB 243 (companion chatbots) and the AI-companion litigation.

Terms (§1, §11):
- Add an explicit "you must be 18 or older" eligibility statement. The old §11
  permitted "Children 13 and over" independently and under-13 via family
  accounts — the exact minor path the app's age gate forbids and the litigation
  epicenter targets.
- Replace §11 "Children and Family Accounts" with an honest "Safety Features"
  section. Removes the Hard Bell auto-notify block ("emergency services and
  trusted contacts are notified first", "cannot be changed") and the
  mandatory-reporting clause ("emergency services or relevant authorities may
  be contacted", "cannot opt out") — both promised an automatic escalation the
  product does not perform. New copy matches the app: Neuron shows you 988 and
  how to reach a contact you chose; it never contacts anyone on your behalf;
  there is no automatic escalation.

Privacy Policy (new):
- Add src/privacy.el + register it in main.el (generation, /legal/privacy route,
  state pointer, sitemap). Fixes the in-app link, which pointed at
  /legal/privacy (404 — no route, no doc existed).
- Ported from docs/legal/privacy-policy-companion-DRAFT.md (2026-07-14):
  local-first, 18+, and never-auto-contact stated verbatim from the locked draft.
- Wire privacy.html through Dockerfile.stage / Dockerfile / deploy.yaml; add the
  secondary /legal/privacy route to server.el for parity.

PENDING ATTORNEY (Daniel) SIGN-OFF before merge/publish. Bracketed legal
decisions (governing law, effective date, retention periods) left for counsel.
2026-07-21 09:54:47 -05:00
13 changed files with 449 additions and 37 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
echo "$CHANGED"
# Asset-only: files that don't require rebuilding the El binary.
# migrations/, scripts/, tests/ are data/infra/test changes — no binary rebuild needed.
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/privacy\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
echo "asset_only=true" >> "$GITHUB_OUTPUT"
echo "=> Asset-only change detected, will use fast path"
@@ -104,7 +104,7 @@ jobs:
- 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.
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
run: touch src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html
# ── El SDK setup ──────────────────────────────────────────────────────
+1 -1
View File
@@ -96,7 +96,7 @@ RUN apt-get update \
&& groupadd -r landing && useradd -r -g landing landing
COPY --from=builder /build/landing /usr/local/bin/landing
COPY src/index.html /srv/landing/index.html
COPY src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html /srv/landing/
COPY src/assets /srv/landing/assets
ENV LANDING_ROOT=/srv/landing
+2 -2
View File
@@ -37,8 +37,8 @@ COPY src/llms.txt /srv/landing/llms.txt
# rewrite them with the freshly-rendered page (extracted JS asset paths,
# updated chat widget, etc.). Without this they stay as their COPY'd root-
# owned shells and the served HTML never reflects post-COPY source edits.
COPY src/about.html src/terms.html src/enterprise-terms.html src/index.html /srv/landing/
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
COPY src/about.html src/terms.html src/privacy.html src/enterprise-terms.html src/index.html /srv/landing/
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/privacy.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
COPY dist/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
+4
View File
@@ -16,6 +16,7 @@
// GET /api/founding-count {"sold":N,"total":N,"remaining":N}
// GET /assets/* static files under $LANDING_ROOT/assets/
// GET /brand/* static files under $LANDING_ROOT/assets/brand/
// GET /legal/privacy privacy.html
// GET /<other> 404 JSON
// Path helpers
@@ -118,6 +119,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
if str_eq(clean, "/legal/terms") {
return fs_read(root_dir() + "/terms.html")
}
if str_eq(clean, "/legal/privacy") {
return fs_read(root_dir() + "/privacy.html")
}
if str_eq(clean, "/about") {
return fs_read(root_dir() + "/about.html")
}
+30 -1
View File
@@ -651,7 +651,8 @@ fn account_roadmap_phase2() -> String {
el_li("", "Process and knowledge packets") +
el_li("", "Local inference via Ollama") +
el_li("", "Additional connectors &mdash; GitHub, Notion, Linear, more") +
el_li("", "Mobile companion app")
el_li("", "Mobile companion app") +
el_li("", "Family accounts")
)
)
}
@@ -691,6 +692,33 @@ fn account_roadmap_section() -> String {
)
}
fn account_family_section() -> String {
let child_attest: String = "<label style=\"display:flex;gap:.5rem;align-items:flex-start;cursor:pointer;margin:.75rem 0\">" +
"<input type=\"checkbox\" id=\"family-attest\" style=\"margin-top:.2rem;accent-color:var(--navy)\">" +
"<span style=\"font-size:.8rem;color:var(--t2)\">I confirm this person is a minor under my care and is using this account under my supervision.</span>" +
"</label>"
el_div(
"id=\"family-section\" style=\"display:none\"",
el_div(
"class=\"card-dark\"",
el_div(
"class=\"acct-section-header\"",
el_p("class=\"card-label\"", "Family members") +
el_p("style=\"font-size:.8125rem;font-weight:300;color:var(--t2);line-height:1.65\"", "Add up to 5 children to your Founding Member plan. $10/month per child.")
) +
el_div("id=\"family-list\"", "") +
el_div(
"id=\"add-child-form\"",
el_input("email", "id=\"child-email\" placeholder=\"Child&#39;s email\" class=\"acct-input\"") +
el_input("number", "id=\"child-dob-year\" placeholder=\"Birth year (e.g. 2012)\" min=\"1990\" max=\"2007\" class=\"acct-input\"") +
child_attest +
el_button("class=\"btn-primary\" onclick=\"addFamilyMember()\" style=\"padding:.75rem 1.5rem\"", "Add family member") +
el_p("id=\"family-msg\" style=\"display:none;font-size:.8rem;margin-top:.5rem\"", "")
)
)
)
}
fn account_badge_section() -> String {
el_div(
"id=\"badge-section\" style=\"display:none\"",
@@ -924,6 +952,7 @@ fn account_dashboard_section() -> String {
account_plan_card() +
account_api_keys_section() +
account_roadmap_section() +
account_family_section() +
account_badge_section() +
account_timeline_card() +
account_download_card() +
+2 -2
View File
@@ -129,8 +129,8 @@ fn comparison_rows() -> String {
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$30/mo</span><br><span style=\"font-size:0.7rem\">Microsoft 365</span></td>" +
"</tr>"
let row10: String = "<tr>" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free trial</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, 14 days</span></td>" +
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free tier</td>" +
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">&#10003;</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, forever</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">GPT-3.5 only</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">&#9679;</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
+71 -2
View File
@@ -1,5 +1,5 @@
// account-dashboard.el -- Account dashboard: session check, plan card.
// Handles onAuthStateChange and plan card rendering.
// account-dashboard.el -- Account dashboard: session check, plan card, family.
// Handles onAuthStateChange, plan card rendering, family member management.
// Compiled with: elc --target=js --bundle --minify --obfuscate
//
// Required globals: window.NEURON_CFG.supabase_url, window.NEURON_CFG.supabase_anon_key
@@ -127,8 +127,77 @@ fn main() -> Void {
var roadmapSection = document.getElementById('roadmap-section');
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
if (plan === 'founding') {
var famSection = document.getElementById('family-section');
if (famSection) famSection.style.display = 'block';
var session = await sb.auth.getSession();
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
if (userEmail) loadFamilyMembers(userEmail);
}
}
async function loadFamilyMembers(parentEmail) {
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
var members = await r.json();
var list = document.getElementById('family-list');
if (!list) return;
if (!members || !members.length) {
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
return;
}
list.innerHTML = members.map(function(m) {
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">'
+ '<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>'
+ '<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>'
+ '<button onclick=\"removeFamilyMember(\'' + m.child_email + '\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>'
+ '</div>';
}).join('');
}
window.addFamilyMember = async function() {
var email = document.getElementById('child-email').value.trim();
var year = document.getElementById('child-dob-year').value;
var attest = document.getElementById('family-attest').checked;
var msg = document.getElementById('family-msg');
if (!email || !year || !attest) {
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Please fill in all fields and confirm the attestation.'; return;
}
if (parseInt(year) < 2008) {
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Child must be under 18. Birth year must be 2008 or later.'; return;
}
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
var r = await fetch('/api/family/invite', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
});
var d = await r.json();
msg.style.display = 'block';
if (d.ok) {
msg.style.color = 'var(--navy)';
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
document.getElementById('child-email').value = '';
document.getElementById('child-dob-year').value = '';
document.getElementById('family-attest').checked = false;
loadFamilyMembers(parentEmail);
} else {
msg.style.color = '#c44';
msg.textContent = d.error || 'Something went wrong.';
}
};
window.removeFamilyMember = async function(childEmail) {
var session = await sb.auth.getSession();
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
await fetch('/api/family/remove', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
});
loadFamilyMembers(parentEmail);
};
function renderUserChip(user) {
var email = user.email || '';
var avatarEl = document.getElementById('user-avatar-el');
+1 -1
View File
@@ -55,7 +55,7 @@ fn local_first() -> String {
let principle3: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:240ms\"",
el_p("class=\"lf-principle-label\"", "No ads. Ever.") +
el_p("class=\"lf-principle-body\"", "Not during your trial. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.")
el_p("class=\"lf-principle-body\"", "Not on the free tier. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I&#39;m not building that.")
)
let principle4: String = el_div(
"class=\"lf-principle reveal\" style=\"transition-delay:360ms\"",
+178 -2
View File
@@ -82,6 +82,7 @@ from styles import { page_open, page_open_seo, page_close }
from about import { about_page }
from founding_badge import { founding_badge, founding_badge_css }
from terms import { terms_page }
from privacy import { privacy_page }
from enterprise_terms import { enterprise_terms_page }
from checkout import { checkout_page }
from safety import { safety }
@@ -582,6 +583,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
+ " <url><loc>https://neurontechnologies.ai/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/privacy</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ " <url><loc>https://neurontechnologies.ai/legal/enterprise-terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
+ "</urlset>\n"
return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body)
@@ -605,6 +607,15 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return "{\"__status__\":404,\"error\":\"not found\"}"
}
// Privacy Policy
if str_eq(path, "/legal/privacy") {
let privacy_path: String = state_get("__privacy_html_file__")
if !str_eq(privacy_path, "") {
return fs_read(privacy_path)
}
return "{\"__status__\":404,\"error\":\"not found\"}"
}
// Enterprise Agreement
if str_eq(path, "/legal/enterprise-terms") {
let ent_path: String = state_get("__enterprise_terms_html_file__")
@@ -628,7 +639,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"Founding Member Checkout — Neuron"
} else {
if str_eq(plan, "free") {
"Start Your Free Trial — Neuron"
"Get Started Free — Neuron"
} else {
"Professional Plan Checkout — Neuron"
}
@@ -637,7 +648,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
"Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
} else {
if str_eq(plan, "free") {
"Start your 14-day free trial. No credit card required. Your AI that remembers you — runs on your machine, never resets."
"Create your free Neuron account. No credit card required. Your AI that remembers you — runs on your machine, never resets."
} else {
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
}
@@ -1991,6 +2002,164 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
return badge_css + badge_html
}
// Family plan POST /api/family/invite
if str_eq(path, "/api/family/invite") {
if !str_eq(method, "POST") {
return "{\"error\":\"POST required\"}"
}
let parent_email: String = json_get(body, "parent_email")
let child_email: String = json_get(body, "child_email")
let child_dob_year_str: String = json_get(body, "child_dob_year")
let attested_str: String = json_get(body, "attested")
if str_eq(parent_email, "") || str_eq(child_email, "") || str_eq(child_dob_year_str, "") {
return "{\"error\":\"parent_email, child_email, and child_dob_year are required\"}"
}
if !str_eq(attested_str, "true") {
return "{\"error\":\"attestation required\"}"
}
let child_dob_year: Int = str_to_int(child_dob_year_str)
if child_dob_year < 2008 {
return "{\"error\":\"Child must be under 18. Birth year must be 2008 or later.\"}"
}
// Check existing family member count
let fam_sb_url: String = state_get("__supabase_project_url__")
let fam_sb_key: String = state_get("__supabase_service_key__")
if str_eq(fam_sb_key, "") {
return "{\"error\":\"not configured\"}"
}
let pe_safe: String = str_replace(str_replace(parent_email, "\\", "\\\\"), "\"", "\\\"")
let ce_safe: String = str_replace(str_replace(child_email, "\\", "\\\\"), "\"", "\\\"")
let members_resp: String = http_get_auth(
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&status=neq.cancelled&select=child_email",
fam_sb_key
)
let member_count: Int = json_array_len(members_resp)
if member_count >= 5 {
return "{\"error\":\"Maximum of 5 family members reached.\"}"
}
// Check child not already added
let dup_resp: String = http_get_auth(
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&child_email=eq." + child_email + "&select=child_email",
fam_sb_key
)
let dup_count: Int = json_array_len(dup_resp)
if dup_count > 0 {
return "{\"error\":\"This child email is already in your family plan.\"}"
}
// Create Stripe customer for child
let fam_stripe_key: String = state_get("__stripe_secret_key__")
if str_eq(fam_stripe_key, "") {
return "{\"error\":\"Stripe not configured\"}"
}
let child_email_safe: String = str_replace(child_email, "@", "%40")
let customer_body: String = "email=" + child_email_safe
let customer_resp: String = http_post_auth("https://api.stripe.com/v1/customers", fam_stripe_key, customer_body)
let child_customer_id: String = json_get(customer_resp, "id")
if str_eq(child_customer_id, "") {
return "{\"error\":\"Failed to create Stripe customer\"}"
}
// Create Stripe subscription for child
let price_id: String = state_get("__stripe_price_family_child__")
if str_eq(price_id, "") {
return "{\"error\":\"Family child price not configured\"}"
}
let parent_email_safe: String = str_replace(parent_email, "@", "%40")
let sub_body: String = "customer=" + child_customer_id
+ "&items[0][price]=" + price_id
+ "&trial_period_days=7"
+ "&metadata[parent_email]=" + parent_email_safe
+ "&metadata[type]=family_child"
let sub_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions", fam_stripe_key, sub_body)
let sub_id: String = json_get(sub_resp, "id")
if str_eq(sub_id, "") {
return "{\"error\":\"Failed to create Stripe subscription\"}"
}
// Generate invite token
let ts_str: String = int_to_str(unix_timestamp())
let email_prefix: String = str_slice(child_email, 0, 4)
let invite_token: String = ts_str + email_prefix
// Insert into family_members table
let fam_row: String = "{\"parent_email\":\"" + pe_safe + "\",\"parent_user_id\":\"\",\"child_email\":\"" + ce_safe + "\",\"child_dob_year\":" + child_dob_year_str + ",\"status\":\"invited\",\"stripe_subscription_id\":\"" + sub_id + "\",\"invite_token\":\"" + invite_token + "\"}"
let fam_insert_resp: String = supabase_insert(fam_sb_url, fam_sb_key, "family_members", fam_row)
println("[family/invite] insert -> " + fam_insert_resp)
// Send invite email
let invite_text: String = "You have been invited to join Neuron by " + parent_email
+ ". Visit https://neurontechnologies.ai/account to set up your account."
let invite_send: String = send_email(
"Neuron <no-reply@neurontechnologies.ai>",
child_email,
"You have been invited to Neuron",
"",
invite_text
)
println("[family/invite] " + invite_send)
return "{\"ok\":true,\"invite_token\":\"" + invite_token + "\"}"
}
// Family plan GET /api/family/members
if str_starts_with(path, "/api/family/members") {
let fml_sb_url: String = state_get("__supabase_project_url__")
let fml_sb_key: String = state_get("__supabase_service_key__")
if str_eq(fml_sb_key, "") {
return "[]"
}
// Extract parent_email from query string
let fml_parent_email: String = ""
let q_idx: Int = str_index_of(path, "parent_email=")
if q_idx >= 0 {
let fml_parent_email = str_slice(path, q_idx + 13, str_len(path))
}
if str_eq(fml_parent_email, "") {
return "{\"error\":\"parent_email required\"}"
}
let fml_resp: String = http_get_auth(
fml_sb_url + "/rest/v1/family_members?parent_email=eq." + fml_parent_email + "&select=child_email,status,child_dob_year,created_at",
fml_sb_key
)
if str_eq(fml_resp, "") {
return "[]"
}
return fml_resp
}
// Family plan POST /api/family/remove
if str_eq(path, "/api/family/remove") {
if !str_eq(method, "POST") {
return "{\"error\":\"POST required\"}"
}
let rem_parent_email: String = json_get(body, "parent_email")
let rem_child_email: String = json_get(body, "child_email")
if str_eq(rem_parent_email, "") || str_eq(rem_child_email, "") {
return "{\"error\":\"parent_email and child_email required\"}"
}
let rem_sb_url: String = state_get("__supabase_project_url__")
let rem_sb_key: String = state_get("__supabase_service_key__")
if str_eq(rem_sb_key, "") {
return "{\"error\":\"not configured\"}"
}
// Fetch the subscription ID
let sub_lookup: String = http_get_auth(
rem_sb_url + "/rest/v1/family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email + "&select=stripe_subscription_id",
rem_sb_key
)
let rem_sub_id: String = json_get(sub_lookup, "0.stripe_subscription_id")
// Cancel Stripe subscription if present
if !str_eq(rem_sub_id, "") {
let rem_stripe_key: String = state_get("__stripe_secret_key__")
if !str_eq(rem_stripe_key, "") {
let cancel_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions/" + rem_sub_id + "/cancel", rem_stripe_key, "")
println("[family/remove] cancel sub " + rem_sub_id + " -> " + cancel_resp)
}
}
// Update status to cancelled
let rpe_safe: String = str_replace(str_replace(rem_parent_email, "\\", "\\\\"), "\"", "\\\"")
let rce_safe: String = str_replace(str_replace(rem_child_email, "\\", "\\\\"), "\"", "\\\"")
let cancel_row: String = "{\"status\":\"cancelled\"}"
supabase_insert(rem_sb_url, rem_sb_key, "family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email, cancel_row)
println("[family/remove] cancelled " + rce_safe + " from " + rpe_safe)
return "{\"ok\":true}"
}
// API key provisioning POST /api/api-keys
// Returns user's stored provider keys (masked) for display on /account.
// Body: { access_token: "<jwt>" }
@@ -2187,6 +2356,7 @@ let stripe_key: String = env("STRIPE_SECRET_KEY")
let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY")
let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING")
let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL")
let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD")
let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET")
let license_api_url: String = env("NEURON_LICENSE_API_URL")
let resend_api_key: String = env("RESEND_API_KEY")
@@ -2227,16 +2397,22 @@ let ent_terms_html_path: String = src_dir + "/enterprise-terms.html"
fs_write(terms_html_path, terms_page())
fs_write(ent_terms_html_path, enterprise_terms_page())
// Generate privacy policy HTML.
let privacy_html_path: String = src_dir + "/privacy.html"
fs_write(privacy_html_path, privacy_page())
// Register with El HTTP runtime.
state_set("__html_file__", html_path)
state_set("__about_html_file__", about_html_path)
state_set("__terms_html_file__", terms_html_path)
state_set("__privacy_html_file__", privacy_html_path)
state_set("__enterprise_terms_html_file__", ent_terms_html_path)
state_set("__src_dir__", src_dir)
state_set("__stripe_secret_key__", stripe_key)
state_set("__stripe_publishable_key__", stripe_pub_key)
state_set("__stripe_price_founding__", stripe_price_founding)
state_set("__stripe_price_professional__", stripe_price_professional)
state_set("__stripe_price_family_child__", family_child_price)
state_set("__license_api_url__", license_api_url)
state_set("__resend_api_key__", resend_api_key)
state_set("__supabase_anon_key__", supabase_anon_key)
+4 -4
View File
@@ -1,4 +1,4 @@
// components/pricing.el - Three pricing cards: Free trial / Professional / Founding Member.
// components/pricing.el - Three pricing cards: Free / Professional / Founding Member.
// Accepts the founding counter values as parameters so main.el can inject
// server-side rendered counts without JS.
@@ -89,14 +89,14 @@ fn pricing(sold: Int, total: Int) -> String {
el_div(
"class=\"pricing-price-row\"",
el_span("class=\"pricing-price\"", "$0") +
el_span("class=\"pricing-cadence\"", "for 14 days")
el_span("class=\"pricing-cadence\"", "forever")
) +
el_p("class=\"pricing-tagline\"", "Try everything free for 14 days. No card required.") +
el_p("class=\"pricing-tagline\"", "Start building your memory. No card required.") +
el_ul("class=\"pricing-features\"", pricing_free_features()) +
el_div("style=\"flex:1\"", "") +
el_div(
"class=\"pricing-cta pricing-cta-ghost\"",
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Start free trial &#8594;")
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Preorder free tier &#8594;")
)
)
+145
View File
@@ -0,0 +1,145 @@
// components/privacy.el - Consumer Privacy Policy page.
// Returns complete HTML using the shared page shell from styles.el.
// Mirrors terms.el. Adapted from docs/legal/privacy-policy-companion-DRAFT.md
// (draft dated 2026-07-14). PENDING ATTORNEY SIGN-OFF before publication.
from styles import { page_open_seo, page_close }
from nav import { nav }
extern fn el_div(attrs: String, children: String) -> String
extern fn el_span(attrs: String, children: String) -> String
extern fn el_h1(attrs: String, text: String) -> String
extern fn el_h2(attrs: String, text: String) -> String
extern fn el_p(attrs: String, children: String) -> String
extern fn el_a(href: String, attrs: String, children: String) -> String
extern fn el_strong(children: String) -> String
fn privacy_page() -> String {
page_open_seo(
"Privacy Policy — Neuron",
"Read the Neuron Privacy Policy. Neuron is local-first: your memory and conversations stay on your device.",
"/legal/privacy",
"The Neuron Privacy Policy — local-first by design. Your memory and conversations stay on your device.",
"false"
) + nav() + privacy_body() + page_close()
}
fn privacy_section_head(num: String, title: String) -> String {
el_div(
"style=\"display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem\"",
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)\"", "&#167; " + num) +
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)\"", title)
)
}
fn privacy_p(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"", text)
}
fn privacy_p_last(text: String) -> String {
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"", text)
}
fn privacy_body() -> String {
let back_link: String = el_div(
"style=\"margin-bottom:3rem\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none\"", "&#8592; Neuron")
)
let page_header: String = el_div(
"style=\"margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem\"",
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Legal") +
el_h1("style=\"font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1\"", "Privacy Policy") +
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t3)\"", "Effective May 1, 2026 &nbsp;&middot;&nbsp; Neuron, LLC")
)
let s1: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("1", "Who We Are") +
privacy_p("Neuron is made by Neuron Technologies / Neuron, LLC (&#34;Neuron,&#34; &#34;we,&#34; &#34;us&#34;). This policy explains what data Neuron handles, where it lives, and what - if anything - leaves your device.") +
privacy_p_last("Neuron is an AI companion with persistent memory. Because it is designed to remember you across time and to feel warm and personal, we hold ourselves to a local-first standard: your relationship with Neuron stays on your machine.")
)
let s2: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("2", "Our Core Principle - Local-First") +
privacy_p_last("Your Neuron memory and knowledge graph are stored " + "<strong style=\"color:var(--t1);font-weight:500\">on your device</strong>" + " (in ~/.neuron) by a local engine running on your own computer. By default, your memories, notes, conversations, documents, and knowledge never leave your device. We cannot see them.")
)
let s3: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("3", "What Data We Handle, and Where It Goes") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Your memory, conversations, and files.</strong> Stored locally on your device. Neuron&#39;s servers do not receive them.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (bring-your-own-key).</strong> To answer you, the Neuron app on your device sends your conversation content directly to the AI provider you choose (for example, Anthropic) using your own API key. This content goes from your machine to that provider; it does not pass through Neuron&#39;s servers, and we do not store or train on it. That provider&#39;s privacy policy and terms govern how they handle it - you are using your own account with them.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (future Neuron-hosted tiers).</strong> If you opt into a paid tier that uses Neuron-hosted models, conversation content needed for inference will be processed on Neuron-operated infrastructure. When that launches, we will not train on it, will not sell it, and will state retention plainly. This is forward-looking and not active today.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">License validation.</strong> On startup the app contacts our licensing server to verify your license status only. We do not transmit your memory or conversations during validation.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Account &amp; purchase data.</strong> If you create an account or buy a tier, we (and our payment processor and sign-in providers) process your email, authentication identity, and payment metadata. We do not receive full card numbers.") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Connectors &amp; imports.</strong> When you connect a third-party service (GitHub, Notion, Linear, Google Drive) or import from another tool, you authorize those flows; the third party&#39;s terms govern their side. Imported content and on-device OCR results are stored locally.") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Website.</strong> The marketing website uses standard anti-abuse and sign-in tooling (for example, Cloudflare Turnstile and OAuth sign-in). The desktop app does not send usage telemetry for ordinary local use.")
)
let s4: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("4", "What We Do Not Do") +
privacy_p_last("We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " sell your data. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " train models on your conversations. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " read your memory or local files.")
)
let s5: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("5", "Safety Features and Crisis Data") +
privacy_p("If Neuron&#39;s safety features are active, distress detection runs on your device. If triggered, Neuron shows you crisis resources (such as 988) and, if you have set a personal safety contact, shows you how to reach that person. " + "<strong style=\"color:var(--t1);font-weight:500\">Neuron does not contact anyone on your behalf - it does not notify your safety contact, 988, 911, or anyone else. There is no automatic escalation.</strong>") +
privacy_p("Your safety contact (a name, relationship, and phone or email you optionally enter) is stored locally on your device in ~/.neuron. It is surfaced only to you, only in a crisis moment, as a reminder of who you trust. It is not transmitted to us.") +
privacy_p_last("Crisis-related conversation content is excluded from Neuron&#39;s persistent memory (&#34;Hard Bell&#34; does not retain it), so distressing moments are not resurfaced to you later. Neuron is not a crisis service and not a medical or clinical provider (see Terms &#167; 11). We process only what is necessary for the feature to function on your device.")
)
let s6: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("6", "Age and Minors") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Neuron is intended for adults; you must be 18 or older to use it</strong>" + " (see Terms &#167; 1). The app asks you to confirm your age before use. We do not knowingly collect personal information from anyone under 18. If we learn that someone under 18 has used Neuron, we will take reasonable steps to delete the information we hold and end that use.")
)
let s7: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("7", "Your Rights and Choices") +
privacy_p_last("Most of your data lives on your device and is under your direct control - you can access, correct, export, and delete it yourself. To delete the account, license, or payment records we hold, contact legal@neurontechnologies.ai. Depending on where you live, you may have GDPR or CCPA/CPRA rights, including the right to know, access, delete, correct, and opt out of the &#34;sale&#34; or &#34;sharing&#34; of your personal information - which we do not do.")
)
let s8: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("8", "Data Retention") +
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Local data</strong>" + " persists on your device until you delete it - you control it directly.") +
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Account, license, and payment records</strong>" + " we hold are retained only as long as needed to provide the service, then deleted on account closure, subject to legal and accounting requirements.")
)
let s9: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("9", "Security") +
privacy_p_last("We use reasonable technical and organizational measures to protect the limited data we hold (account, license, and payment metadata). Because your memory and conversations stay on your device, the security of that device is also important - keep it protected.")
)
let s10: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("10", "Changes to This Policy") +
privacy_p_last("We will post material changes here and, where appropriate, notify you in-app.")
)
let s11: String = el_div(
"style=\"margin-bottom:3rem\"",
privacy_section_head("11", "Contact") +
privacy_p_last("legal@neurontechnologies.ai - Neuron Technologies / Neuron, LLC.")
)
let footer_links: String = el_div(
"style=\"margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap\"",
el_a("/", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "&larr; Home") +
el_a("/legal/terms", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "Terms of Service &rarr;")
)
el_div(
"style=\"max-width:720px;margin:0 auto;padding:6rem 2.5rem 8rem\"",
back_link +
page_header +
s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 +
footer_links
)
}
+2 -2
View File
@@ -78,9 +78,9 @@ fn page_head() -> String {
return page_head_base()
+ page_seo_block(
"Neuron — The AI That Remembers You",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start with a 14-day free trial — no credit card required.",
"Every AI resets when you close the tab. Neuron doesn&#39;t. Runs on your machine. Remembers everything. Start free — no credit card required.",
"/",
"Every other AI forgets you. Neuron doesn&#39;t. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. 14-day free trial."
"Every other AI forgets you. Neuron doesn&#39;t. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. Free tier available."
)
+ page_schema()
}
+7 -18
View File
@@ -59,6 +59,7 @@ fn terms_body() -> String {
"style=\"margin-bottom:3rem\"",
terms_section_head("1", "Using Neuron") +
terms_p("Using Neuron means you&#39;ve read these terms and agree to them. That&#39;s it. No buried consent, no dark patterns. If something here doesn&#39;t sit right with you, email us before using the product - we&#39;ll talk through it.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">You must be 18 or older to use Neuron.</strong> Neuron is intended for adults. By using it you represent that you are at least 18 years old and able to form a binding contract. The app asks you to confirm your age before you can use it, and we do not knowingly permit anyone under 18 to use Neuron.") +
terms_p_last("These terms cover every version of Neuron: free, Professional, and Founding Member. Enterprise use has its own agreement. If you&#39;re deploying Neuron for an organization, that&#39;s the one you want.")
)
@@ -136,26 +137,14 @@ fn terms_body() -> String {
terms_p_last("Founding Member licenses are locked. I won&#39;t retroactively change what you signed up for.")
)
let hard_bell_block: String = el_div(
"style=\"margin:1.5rem 0;padding:1.5rem;border-left:3px solid rgba(0,82,160,.35);background:rgba(0,82,160,.04)\"",
el_p("style=\"font-family:var(--body);font-weight:600;font-size:0.875rem;color:var(--t1);margin-bottom:0.875rem;letter-spacing:0.04em\"", "Hard Bell - Required acknowledgment for family accounts") +
terms_p("Federal data shows approximately 89% of child maltreatment is perpetrated by a parent or caregiver (HHS, Child Maltreatment 2023). The person listed as the primary contact is not always a safe contact. This system is built with that reality in mind.") +
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.875rem;color:var(--t1);line-height:1.8\"",
"When you create a family account that includes a minor, you are agreeing unconditionally to the following: " +
"<strong>if a safety signal is triggered by a child&#39;s account, the parent or guardian named on the family account will not be the first contact reached.</strong>" +
" Emergency services and independently designated trusted contacts are notified first. This is not a setting that can be changed. By creating a family account, you accept this structure."
)
)
let s11: String = el_div(
"style=\"margin-bottom:3rem\"",
terms_section_head("11", "Children and Family Accounts") +
terms_p("Children 13 and over may use Neuron independently. Children under 13 may only use Neuron as part of a family account established by a parent or legal guardian. By setting up a family account that includes a minor, the parent or guardian provides verifiable consent for the child&#39;s use and accepts these terms on the child&#39;s behalf.") +
terms_p("When a child&#39;s Neuron instance is part of a family account, certain information - including usage activity and relevant context - may be shared from the child&#39;s local instance to the parent&#39;s local instance. This sharing happens device-to-device and does not pass through or get stored on Neuron&#39;s servers.") +
hard_bell_block +
el_p("style=\"font-family:var(--body);font-weight:500;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem\"", "Mandatory reporting - applies to all accounts.") +
terms_p("By creating any Neuron account, you acknowledge and agree that: " + "<strong style=\"color:var(--t1);font-weight:500\">if Neuron detects credible, specific indicators of intended harm to a person - including harm to the account holder themselves - emergency services or relevant authorities may be contacted.</strong>" + " This is not limited to family accounts. It applies to every user. This is not a privacy violation. It is the only honest answer to what a system that knows you this well is obligated to do when someone is in genuine danger. You cannot opt out of this. If you are unwilling to agree, do not create an account.") +
terms_p_last("Parents control what is visible to them through their local instance and can revoke family account access at any time. If you believe a child is using Neuron outside of a family account, contact legal@neurontechnologies.ai and we will address it promptly.")
terms_section_head("11", "Safety Features") +
terms_p("Neuron is for adults 18 and older (see &#167; 1). It is not a crisis line, not a medical or mental-health service, and not an emergency responder. In an emergency, contact 911 or your local emergency number, or call or text 988 (the Suicide &amp; Crisis Lifeline) directly.") +
terms_p("Neuron includes optional safety features intended to help if you are ever in distress. It is important that you understand exactly what they do and do not do.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do.</strong> If Neuron&#39;s on-device distress detection is triggered, Neuron shows you crisis resources - prominently the 988 Suicide &amp; Crisis Lifeline (call, text, or chat) - and, if you have chosen a personal safety contact, it shows you how to reach that person. You remain in control of whether and how you reach out.") +
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do not do. Neuron does not contact anyone on your behalf. It does not call, text, email, or otherwise notify your safety contact, 988, 911, or any other person or service. There is no automatic escalation.</strong> The safety contact you set is stored locally on your device and is surfaced only to you, as a reminder of who you trust - Neuron never reaches out to them for you.") +
terms_p_last("These features are provided &#34;AS IS.&#34; Distress detection is imperfect: it may fail to detect distress, may raise false alarms, and must not be relied upon in an emergency. To the maximum extent permitted by law, Neuron, LLC is not liable for any outcome relating to the presence, absence, accuracy, timing, or performance of any safety feature.")
)
let s12: String = el_div(