Sign in with Verae on register/app pages (DEC-657): free anonymous lookup first, new documents stamped with a per-tab key from the shared device sign-in (sessionStorage only), price + balance confirmed; receipt links no longer call stamp; app.html old localStorage login removed; jsPDF/qrcode self-hosted; index snippets send a key
This commit is contained in:
parent
1ca834900e
commit
8a532c6585
1 changed files with 948 additions and 212 deletions
133
tests/web/signin-e2e.mjs
Normal file
133
tests/web/signin-e2e.mjs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// E2E for DEC-657 against live mcp.verae.ai + zap.verae.ai, test customer cust_verae_test_billing.
|
||||
// PRE=1 serves the new pages/assets from ../site via request interception (before deploying them).
|
||||
import puppeteer from "puppeteer-core";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
const SITE = path.resolve("../site");
|
||||
const PRE = process.env.PRE === "1";
|
||||
const SESSION = process.env.PORTAL_SESSION; // temp portal session for the test customer (deleted afterwards)
|
||||
const KNOWN = { sha: "e1dcc8e28577df3282d0d3f04038df1ff23fa462b7a6c29a9697e48393a4579c", rid: "QmS6NoQjR42YgRAmZJAwzQ2BQSycL2pKTATfr6dX1X2TTp" };
|
||||
const out = { checks: [], keys: [] };
|
||||
const ok = (name, cond, extra) => { out.checks.push({ name, ok: !!cond, extra }); console.log((cond ? "ok " : "FAIL ") + name + (extra ? " :: " + String(extra).slice(0, 200) : "")); if (!cond) process.exitCode = 1; };
|
||||
const types = { ".html": "text/html", ".js": "text/javascript" };
|
||||
|
||||
const browser = await puppeteer.launch({ executablePath: "/usr/bin/google-chrome", headless: "new", args: ["--no-sandbox"] });
|
||||
browser.on("targetcreated", async (t) => { if (t.type() === "page") { const p = await t.page(); if (p && p.url().includes("/portal/device.html")) { out.popup = p.url(); await p.close().catch(() => {}); } } });
|
||||
|
||||
async function page(url) {
|
||||
const p = await browser.newPage();
|
||||
const ext = [];
|
||||
if (PRE) {
|
||||
await p.setRequestInterception(true);
|
||||
p.on("request", (r) => {
|
||||
const u = new URL(r.url());
|
||||
if (u.host === "mcp.verae.ai" && /^\/(register|app|index)\.html$|^\/assets\/(verae-signin\.js|vendor\/.+)$/.test(u.pathname)) {
|
||||
const f = path.join(SITE, u.pathname);
|
||||
return r.respond({ status: 200, contentType: types[path.extname(f)] || "application/octet-stream", body: fs.readFileSync(f) });
|
||||
}
|
||||
r.continue();
|
||||
});
|
||||
}
|
||||
p.on("request", (r) => { const h = new URL(r.url()).host; if (r.resourceType() === "script" && !/(^|\.)verae\.ai$/.test(h)) ext.push(r.url()); });
|
||||
p.on("console", (m) => { if (m.type() === "error") console.log(" [console]", m.text().slice(0, 200)); });
|
||||
p.dialogs = [];
|
||||
p.on("dialog", async (d) => { p.dialogs.push(d.message()); await d.accept(); });
|
||||
await p.goto(url, { waitUntil: "networkidle2" });
|
||||
p.ext = ext;
|
||||
return p;
|
||||
}
|
||||
const status = (p) => p.$eval("#status", (e) => e.textContent);
|
||||
async function waitStatus(p, re, ms = 180000) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < ms) { const s = await status(p); if (re.test(s)) return s; await new Promise((r) => setTimeout(r, 500)); }
|
||||
return "TIMEOUT: " + (await status(p));
|
||||
}
|
||||
async function approveWhenShown(p) {
|
||||
await p.waitForSelector("#veraeUserCode", { visible: true, timeout: 60000 });
|
||||
const code = await p.$eval("#veraeUserCode", (e) => e.textContent);
|
||||
const href = await p.$eval("#veraeSignInLink", (e) => e.href);
|
||||
ok("sign-in shows code + portal link", /^[A-Z]{4}-[A-Z]{4}$/.test(code) && href.startsWith("https://zap.verae.ai/portal/device.html?user_code="), code + " " + href);
|
||||
const r = await fetch("https://zap.verae.ai/portal/api/device/approve", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + SESSION }, body: JSON.stringify({ user_code: code }) });
|
||||
ok("approved in the portal as the test customer", r.ok, r.status);
|
||||
}
|
||||
async function storage(p) {
|
||||
return p.evaluate(() => ({ ss: Object.assign({}, sessionStorage), ls: Object.assign({}, localStorage), cookie: document.cookie }));
|
||||
}
|
||||
function tmpFile(tag) {
|
||||
const f = `/tmp/verae-e2e-${tag}-${Date.now()}.txt`;
|
||||
fs.writeFileSync(f, `DEC-657 e2e ${tag} ${crypto.randomUUID()}\n`);
|
||||
return { f, sha: crypto.createHash("sha256").update(fs.readFileSync(f)).digest("hex") };
|
||||
}
|
||||
|
||||
// 1) register.html deep link: anonymous lookup, no stamp, no key
|
||||
let p = await page(`https://mcp.verae.ai/register.html?sha256=${KNOWN.sha}`);
|
||||
let s = await waitStatus(p, /Lookup complete|Error|error|No receipt/i, 60000);
|
||||
const row = await p.$eval("#tbody", (e) => e.textContent);
|
||||
ok("register.html receipt link: anonymous lookup works", /Lookup complete/.test(s) && row.includes(KNOWN.rid), s);
|
||||
ok("no key used for the lookup", Object.keys((await storage(p)).ss).length === 0);
|
||||
ok("signed-out hint shown", (await p.$eval("[data-testid=signin-status]", (e) => e.textContent)).includes("needs a Verae account"));
|
||||
|
||||
// 2) register a new document: sign in (device flow), price + balance confirmed, stamped with the key
|
||||
const A = tmpFile("register");
|
||||
await p.goto("https://mcp.verae.ai/register.html", { waitUntil: "networkidle2" });
|
||||
await (await p.$("#fileInput")).uploadFile(A.f);
|
||||
await p.click("#runBtn");
|
||||
await approveWhenShown(p);
|
||||
s = await waitStatus(p, /^Finished|Not registered|Error/);
|
||||
ok("register.html: new document registered", /^Finished: 1 ok/.test(s), s);
|
||||
ok("price + balance confirmed before charging", p.dialogs.length === 1 && /10 credits each, 10 credits in total/.test(p.dialogs[0]) && /Balance: \d+ credits/.test(p.dialogs[0]), p.dialogs[0]);
|
||||
let st = await storage(p);
|
||||
const keyA = st.ss["verae.signin.key"]; out.keys.push(keyA);
|
||||
ok("key in sessionStorage only", /^xVerae-/.test(keyA || "") && !JSON.stringify(st.ls).includes("xVerae-") && !st.cookie.includes("xVerae"), Object.keys(st.ls).join(","));
|
||||
ok("account line shows price and balance", /price per new document 10 credits · balance [\d,]+ credits/.test(await p.$eval("[data-testid=signin-status]", (e) => e.textContent)));
|
||||
const ridA = await p.$eval("#tbody", (e) => e.textContent);
|
||||
out.newA = A.sha;
|
||||
// 3) same document again: found by the free lookup, no dialog, no charge
|
||||
const d0 = p.dialogs.length;
|
||||
await p.click("#clearBtn");
|
||||
await (await p.$("#fileInput")).uploadFile(A.f);
|
||||
await p.click("#runBtn");
|
||||
s = await waitStatus(p, /^Finished|Error/);
|
||||
ok("same document again: found, no confirmation, no stamp", /^Finished: 1 ok/.test(s) && p.dialogs.length === d0, s);
|
||||
// 4) sign out revokes the key
|
||||
await p.click("#veraeSignOutBtn");
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
st = await storage(p);
|
||||
const who = await fetch("https://zap.verae.ai/v1/whoami", { headers: { authorization: "Bearer " + keyA } });
|
||||
ok("sign out: key forgotten and revoked", !st.ss["verae.signin.key"] && who.status === 401, who.status);
|
||||
ok("register.html loads no third-party scripts (fonts CSS only)", p.ext.length === 0, p.ext.join(" "));
|
||||
await p.close();
|
||||
|
||||
// 5) app.html: legacy localStorage credentials are wiped; register flow with its own sign-in
|
||||
p = await page("https://mcp.verae.ai/app.html");
|
||||
await p.evaluate(() => localStorage.setItem("verae.auth", JSON.stringify({ username: "x", password: "y", accessToken: "z" })));
|
||||
await p.reload({ waitUntil: "networkidle2" });
|
||||
ok("app.html removes the old localStorage username/password/JWT", (await p.evaluate(() => localStorage.getItem("verae.auth"))) === null);
|
||||
ok("app.html has no password/JWT fields", (await p.$("#password")) === null && (await p.$("#accessToken")) === null);
|
||||
const B = tmpFile("app");
|
||||
await (await p.$("#fileInput")).uploadFile(B.f);
|
||||
await p.click("#runBtn");
|
||||
await approveWhenShown(p);
|
||||
s = await waitStatus(p, /^Finished|Not registered|Error/);
|
||||
ok("app.html: new document registered", /^Finished: 1 ok/.test(s), s);
|
||||
ok("app.html: price + balance confirmed", p.dialogs.length === 1 && /10 credits each/.test(p.dialogs[0]), p.dialogs[0]);
|
||||
st = await storage(p);
|
||||
const keyB = st.ss["verae.signin.key"]; out.keys.push(keyB);
|
||||
ok("app.html key in sessionStorage only", /^xVerae-/.test(keyB || "") && !JSON.stringify(st.ls).includes("xVerae-") && !st.cookie.includes("xVerae"));
|
||||
// verify mode stays anonymous
|
||||
await p.click("#veraeSignOutBtn"); await new Promise((r) => setTimeout(r, 1500));
|
||||
await p.goto(`https://mcp.verae.ai/app.html?sha256=${B.sha}`, { waitUntil: "networkidle2" });
|
||||
s = await waitStatus(p, /Lookup complete|Not found|Error|Invalid/i, 60000);
|
||||
ok("app.html receipt link (signed out): anonymous lookup + verify", /Lookup complete/.test(s), s);
|
||||
ok("app.html loads no third-party scripts", p.ext.length === 0, p.ext.join(" "));
|
||||
out.newB = B.sha;
|
||||
await p.close();
|
||||
|
||||
// 6) index.html docs mention the key for stamp
|
||||
p = await page("https://mcp.verae.ai/index.html");
|
||||
ok("index.html stamp snippet sends a Verae API key", (await p.content()).includes("Authorization: Bearer YOUR_VERAE_API_KEY"));
|
||||
await p.close();
|
||||
await browser.close();
|
||||
fs.writeFileSync("/tmp/verae-e2e-result.json", JSON.stringify(out, null, 2));
|
||||
console.log(process.exitCode ? "E2E FAIL" : "E2E OK", "new digests:", out.newA, out.newB);
|
||||
Loading…
Reference in a new issue