From 4b5663e3375e7173aa57240334d7154710ec0253 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:00:37 -0700 Subject: [PATCH 1/2] feat(site): optional email on /try, recorded via amw-test-signup After a key is shown, one fire and forget POST sends wallet_id, optional email and UTM/referrer to the amw-test-signup function. api_key is never sent. Adds the Supabase origin to /try CSP connect-src, with tests. --- site/test_try_page.mjs | 98 +++++++++++++++++++++++++++--- site/try/index.html | 8 ++- site/try/try.css | 21 +++++++ site/try/try.js | 34 +++++++++-- site/vercel.json | 2 +- tests/test_site_agent_interface.py | 6 +- 6 files changed, 153 insertions(+), 16 deletions(-) diff --git a/site/test_try_page.mjs b/site/test_try_page.mjs index e3146670..8f8ed029 100644 --- a/site/test_try_page.mjs +++ b/site/test_try_page.mjs @@ -6,6 +6,8 @@ import { createContext, runInContext } from "node:vm"; const script = readFileSync(new URL("./try/try.js", import.meta.url), "utf8"); const captureScript = readFileSync(new URL("./utm-capture.js", import.meta.url), "utf8"); const html = readFileSync(new URL("./try/index.html", import.meta.url), "utf8"); +const DEMO_KEY_URL = "https://api.thisisatest.tech/v1/demo/keys"; +const SIGNUP_URL = "https://cqtlzicxmaklslvxderm.supabase.co/functions/v1/amw-test-signup"; function makePage(fetchResponse, options = {}) { const ids = [ @@ -14,12 +16,14 @@ function makePage(fetchResponse, options = {}) { "try-key-result", "try-api-key", "try-wallet-id", + "try-email", + "try-email-error", "copy-try-key", "copy-try-wallet", ]; const elements = new Map(ids.map((id) => [id, { disabled: false, - hidden: id === "try-key-result", + hidden: id === "try-key-result" || id === "try-email-error", textContent: "", value: "", listeners: {}, @@ -46,7 +50,11 @@ function makePage(fetchResponse, options = {}) { URL, async fetch(url, request) { requests.push([url, request]); - return fetchResponse(); + if (url === DEMO_KEY_URL) return fetchResponse(); + if (url === SIGNUP_URL) { + return options.signupResponse ? options.signupResponse() : { status: 201 }; + } + throw new Error(`unexpected fetch URL: ${url}`); }, navigator: { clipboard: { async writeText(value) { copies.push(value); } }, @@ -80,6 +88,20 @@ function makePage(fetchResponse, options = {}) { }; } +function signupRequests(page) { + return page.requests.filter(([url]) => url === SIGNUP_URL); +} + +function signupPayload(page) { + const requests = signupRequests(page); + assert.equal(requests.length, 1); + const [, request] = requests[0]; + assert.equal(request.method, "POST"); + assert.equal(request.credentials, "omit"); + assert.equal(request.headers["Content-Type"], "application/json"); + return JSON.parse(request.body); +} + function issued(key = "test_key", wallet = "wallet_test") { return { status: 201, @@ -146,15 +168,15 @@ function pageTokens(markup) { return { tags, visibleMainText }; } -test("one empty credential free POST shows key and wallet once and copies both", async () => { +test("one demo POST shows key and wallet once, copies both, and sends one credential free signup", async () => { const key = "demo_"; const wallet = "wallet_"; const page = makePage(async () => issued(key, wallet)); await page.click("get-try-key"); - assert.equal(page.requests.length, 1); + assert.equal(page.requests.length, 2); const [url, request] = page.requests[0]; - assert.equal(url, "https://api.thisisatest.tech/v1/demo/keys"); + assert.equal(url, DEMO_KEY_URL); assert.equal(request.method, "POST"); assert.equal(request.credentials, "omit"); assert.deepEqual(Object.keys(request).sort(), ["credentials", "method"]); @@ -165,13 +187,18 @@ test("one empty credential free POST shows key and wallet once and copies both", const displayed = [...page.elements.values()].map((element) => `${element.value}${element.textContent}`).join(" "); assert.equal(displayed.split(key).length - 1, 1); assert.equal(displayed.split(wallet).length - 1, 1); + const payload = signupPayload(page); + assert.deepEqual(payload, { wallet_id: wallet }); + assert.equal(Object.hasOwn(payload, "api_key"), false); + assert.equal(JSON.stringify(payload).includes(key), false); await page.click("copy-try-key"); await page.click("copy-try-wallet"); assert.deepEqual(page.copies, [key, wallet]); assert.doesNotMatch(page.elements.get("try-key-status").textContent, /[-\u2013\u2014]/); await page.click("get-try-key"); - assert.equal(page.requests.length, 1); + assert.equal(page.requests.length, 2); + assert.equal(signupRequests(page).length, 1); assert.equal(page.session.size, 0); }); @@ -199,6 +226,58 @@ test("stored UTM is overridden by URL values and first external referrer is reta }); assert.deepEqual(JSON.parse(page.session.get("amwDemoKeyUtm")), JSON.parse(request.body)); assert.equal([...page.session.values()].some((value) => value.includes("test_key")), false); + assert.deepEqual(signupPayload(page), { + wallet_id: "wallet_test", + utm_source: "newsletter", + utm_campaign: "launch", + utm_term: "receipts", + referrer: "https://first.example/landing", + }); +}); + +test("valid optional email is trimmed and sent with wallet attribution", async () => { + const page = makePage(async () => issued("secret_key", "wallet_email"), { + search: "?utm_source=docs", + referrer: "https://partner.example/guide", + }); + page.elements.get("try-email").value = " user@example.com "; + + await page.click("get-try-key"); + assert.deepEqual(signupPayload(page), { + wallet_id: "wallet_email", + email: "user@example.com", + utm_source: "docs", + referrer: "https://partner.example/guide", + }); + assert.equal(page.elements.get("try-email-error").textContent, ""); + assert.equal(page.elements.get("try-email-error").hidden, true); +}); + +test("invalid optional email is omitted while wallet signup and key display continue", async () => { + for (const email of ["not-an-email", `${"a".repeat(250)}@x.co`]) { + const page = makePage(async () => issued("secret_key", "wallet_invalid")); + page.elements.get("try-email").value = email; + + await page.click("get-try-key"); + assert.deepEqual(signupPayload(page), { wallet_id: "wallet_invalid" }); + assert.notEqual(page.elements.get("try-email-error").textContent, ""); + assert.equal(page.elements.get("try-email-error").hidden, false); + assert.equal(page.elements.get("try-key-result").hidden, false); + assert.equal(shownValue(page.elements.get("try-api-key")), "secret_key"); + } +}); + +test("signup failure is silent and leaves the issued key visible", async () => { + const page = makePage(async () => issued("secret_key", "wallet_failure"), { + signupResponse: async () => { throw new Error("signup unavailable"); }, + }); + + await page.click("get-try-key"); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(signupPayload(page), { wallet_id: "wallet_failure" }); + assert.equal(page.elements.get("try-key-result").hidden, false); + assert.equal(shownValue(page.elements.get("try-api-key")), "secret_key"); + assert.equal(page.elements.get("try-key-status").textContent, "Test key ready. Copy it now."); }); test("UTM and referrer are capped before storage and request", async () => { @@ -245,7 +324,8 @@ test("failed requests leave the key hidden and allow retry", async () => { assert.ok(page.elements.get("try-key-status").textContent); assert.doesNotMatch(page.elements.get("try-key-status").textContent, /[-\u2013\u2014]/); await page.click("get-try-key"); - assert.equal(page.requests.length, 2); + assert.equal(page.requests.filter(([url]) => url === DEMO_KEY_URL).length, 2); + assert.equal(signupRequests(page).length, 1); assert.equal(page.elements.get("try-key-result").hidden, false); } @@ -283,6 +363,10 @@ test("page uses external assets and visible copy contains no dashes", () => { } } assert.ok(openingTags.some((tag) => tag.name === "button" && tag.raw.includes('id="get-try-key"'))); + assert.ok(openingTags.some((tag) => tag.name === "label" && tag.raw.includes('for="try-email"'))); + assert.ok(openingTags.some((tag) => tag.name === "input" && tag.raw.includes('id="try-email"') && tag.raw.includes('type="email"') && tag.raw.includes('autocomplete="email"'))); + assert.ok(visibleMainText.includes("Email (optional)")); + assert.ok(visibleMainText.includes("Leave it blank")); assert.ok(visibleMainText.includes("Get a free test key")); for (const id of ["copy-try-key", "copy-try-wallet"]) { assert.ok(openingTags.some((tag) => tag.name === "button" && tag.raw.includes(`id="${id}"`))); diff --git a/site/try/index.html b/site/try/index.html index 286f545e..dd928ead 100644 --- a/site/try/index.html +++ b/site/try/index.html @@ -54,13 +54,13 @@ @@FONT_PRELOADS@@ - + @@VERCEL_ANALYTICS_SCRIPTS@@ - + @@ -73,6 +73,10 @@

Try one governed tool call.

+ + +

Add your email if you want updates from us about AMW. Leave it blank to continue without sharing an email.

+

No account or payment needed.

diff --git a/site/try/try.css b/site/try/try.css index 5024e90a..104195e3 100644 --- a/site/try/try.css +++ b/site/try/try.css @@ -31,6 +31,27 @@ cursor: default; } +.try-page .email-input { + box-sizing: border-box; + width: 100%; + padding: 0.7rem 0.8rem; + border: 1px solid var(--paper-line); + border-radius: var(--radius-sm); + background: var(--paper-shade); + color: var(--paper-ink); + font: inherit; +} + +.try-page .email-help, +.try-page .email-error { + margin: 0.45rem 0 1rem; + color: var(--paper-dim); +} + +.try-page .email-error[hidden] { + display: none; +} + #try-key-status { margin: 0.9rem 0 0; color: var(--paper-dim); diff --git a/site/try/try.js b/site/try/try.js index 19b3d8ac..fd08a6ff 100644 --- a/site/try/try.js +++ b/site/try/try.js @@ -4,6 +4,8 @@ const result = document.getElementById("try-key-result"); const keyInput = document.getElementById("try-api-key"); const walletInput = document.getElementById("try-wallet-id"); + const emailInput = document.getElementById("try-email"); + const emailError = document.getElementById("try-email-error"); const UTM_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term"]; const UTM_STORAGE_KEY = "amwDemoKeyUtm"; const UTM_MAX_LENGTH = 128; @@ -93,9 +95,33 @@ persistAttribution(captureAttribution()); - function onKeyIssued({ wallet_id, fields }) { - // Reserved for an optional signup insert in a later change. - // Never pass the API key to this hook or store it in attribution. + async function onKeyIssued({ wallet_id, fields }) { + const signup = { wallet_id }; + for (const key of UTM_PARAMS) { + if (fields[key]) signup[key] = fields[key]; + } + if (fields.referrer) signup.referrer = fields.referrer; + + const email = emailInput.value.trim(); + if (email) { + if (email.length <= 254 && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { + signup.email = email; + } else { + emailError.textContent = "That email address was not included. Your test key is ready."; + emailError.hidden = false; + } + } + + try { + await fetch("https://cqtlzicxmaklslvxderm.supabase.co/functions/v1/amw-test-signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "omit", + body: JSON.stringify(signup), + }); + } catch { + // Signup is optional and never interrupts key issuance. + } } issueButton.addEventListener("click", async () => { @@ -153,7 +179,7 @@ walletInput.value = data.wallet_id; result.hidden = false; status.textContent = "Test key ready. Copy it now."; - onKeyIssued({ wallet_id: data.wallet_id, fields }); + void onKeyIssued({ wallet_id: data.wallet_id, fields }); }); async function copyValue(input, label) { diff --git a/site/vercel.json b/site/vercel.json index 1285896b..290409de 100644 --- a/site/vercel.json +++ b/site/vercel.json @@ -16,7 +16,7 @@ }, { "key": "Content-Security-Policy", - "value": "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'self'; form-action 'none'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self' https://vitals.vercel-insights.com https://api.thisisatest.tech; manifest-src 'self'; upgrade-insecure-requests" + "value": "default-src 'self'; base-uri 'none'; object-src 'none'; frame-ancestors 'self'; form-action 'none'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self' https://vitals.vercel-insights.com https://api.thisisatest.tech https://cqtlzicxmaklslvxderm.supabase.co; manifest-src 'self'; upgrade-insecure-requests" } ] }, diff --git a/tests/test_site_agent_interface.py b/tests/test_site_agent_interface.py index db930cc5..348a9489 100644 --- a/tests/test_site_agent_interface.py +++ b/tests/test_site_agent_interface.py @@ -1417,7 +1417,8 @@ def test_site_sends_a_content_security_policy_and_hsts() -> None: assert policy["script-src"] == "script-src 'self'" assert policy["connect-src"] == ( "connect-src 'self' https://vitals.vercel-insights.com " - "https://api.thisisatest.tech" + "https://api.thisisatest.tech " + "https://cqtlzicxmaklslvxderm.supabase.co" ) assert "'unsafe-inline'" not in values["Content-Security-Policy"] # Typography is self-hosted from /fonts, so neither font CDN is permitted. @@ -1463,7 +1464,8 @@ def test_built_demo_key_page_has_the_key_controls_and_api_connection(tmp_path) - policy = _csp_policy(values["Content-Security-Policy"]) assert policy["connect-src"] == ( "connect-src 'self' https://vitals.vercel-insights.com " - "https://api.thisisatest.tech" + "https://api.thisisatest.tech " + "https://cqtlzicxmaklslvxderm.supabase.co" ) From 16211afe955024dfd31737813dcaffa416a0b2e6 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:48:04 -0700 Subject: [PATCH 2/2] fix(site): keep signup request alive through navigation --- site/try/try.js | 1 + 1 file changed, 1 insertion(+) diff --git a/site/try/try.js b/site/try/try.js index fd08a6ff..7b9aa92d 100644 --- a/site/try/try.js +++ b/site/try/try.js @@ -117,6 +117,7 @@ method: "POST", headers: { "Content-Type": "application/json" }, credentials: "omit", + keepalive: true, body: JSON.stringify(signup), }); } catch {