Public: anyone can read this; the crew can change it.
/verify, /recover and /claim name their Server-Timing phases and answer under 50 ms #4324
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4317-verify-recover-and-claim-name-their-serv → main 3 commits fork
5 files
changed +77
−13
src/web/pages/claim.ts +1 −1
| @@ -230,7 +230,7 @@ const fragmentJs = /* js */ ` | ||
| 230 | 230 | export async function claimFragmentPage(ctx: Ctx): Promise<Response> { |
| 231 | 231 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 232 | 232 | const spawned = await timePhaseAsync(ctx.request, "spawned", () => spawnedOrg(ctx.app)); |
| 233 | if (spawned === undefined) return dead(ctx, "unknown"); | |
| 233 | if (spawned === undefined) return timePhase(ctx.request, "render", () => dead(ctx, "unknown")); | |
| 234 | 234 | const { spawner } = timePhase(ctx.request, "owners", () => owners(ctx, spawned)); |
| 235 | 235 | const rendering = performance.now(); |
| 236 | 236 | const response = page({ |
src/web/pages/passkeys.ts +8 −6
| @@ -234,9 +234,11 @@ const NOT_VALID = "This link has expired, been used, or is not valid. Ask for a | ||
| 234 | 234 | |
| 235 | 235 | // GET /recover — the request form, or (with ?token=) the new-passkey step. |
| 236 | 236 | export function recoverPage(ctx: Ctx): Response { |
| 237 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 238 | const render = (draw: () => Response): Response => timePhase(ctx.request, "render", draw); | |
| 237 | 239 | const token = ctx.url.searchParams.get("token"); |
| 238 | 240 | if (token === null) { |
| 239 | return page({ | |
| 241 | return render(() => page({ | |
| 240 | 242 | ctx, |
| 241 | 243 | title: "Recover your account", |
| 242 | 244 | body: html`<div class="page-head"><h1>Recover your account</h1></div> |
| @@ -246,12 +248,12 @@ export function recoverPage(ctx: Ctx): Response { | ||
| 246 | 248 | <input id="recover-email" name="email" type="email" maxlength="254" autocomplete="email" required /></div> |
| 247 | 249 | <div class="form-actions"><button class="btn" data-variant="primary">Send the link</button></div> |
| 248 | 250 | </form>`, |
| 249 | }); | |
| 251 | })); | |
| 250 | 252 | } |
| 251 | const user = recoveryAccount(ctx.app, token); | |
| 253 | const user = timePhase(ctx.request, "link", () => recoveryAccount(ctx.app, token)); | |
| 252 | 254 | if (user === undefined) |
| 253 | return page({ ctx, title: "Link not valid", body: html`<div class="page-head"><h1>Link not valid</h1></div><p>${NOT_VALID}</p><p><a href="/recover">Ask for a new link</a></p>` }, 400); | |
| 254 | return page({ | |
| 255 | return render(() => page({ ctx, title: "Link not valid", body: html`<div class="page-head"><h1>Link not valid</h1></div><p>${NOT_VALID}</p><p><a href="/recover">Ask for a new link</a></p>` }, 400)); | |
| 256 | return render(() => page({ | |
| 255 | 257 | ctx, |
| 256 | 258 | title: "Register a new passkey", |
| 257 | 259 | scriptJs: passkeyJs, |
| @@ -262,7 +264,7 @@ export function recoverPage(ctx: Ctx): Response { | ||
| 262 | 264 | <p class="error-note" id="passkey-recover-note" role="status"></p> |
| 263 | 265 | <noscript><p class="muted small">Registering a passkey needs JavaScript.</p></noscript> |
| 264 | 266 | <div class="form-actions"><button type="button" id="passkey-recover" class="btn" data-variant="primary">Register and sign in</button></div>`, |
| 265 | }); | |
| 267 | })); | |
| 266 | 268 | } |
| 267 | 269 | |
| 268 | 270 | // POST /recover — ask for a link. The same page for every address. |
src/web/pages/site.ts +4 −3
| @@ -561,8 +561,9 @@ export async function signinSubmit(ctx: Ctx): Promise<Response> { | ||
| 561 | 561 | |
| 562 | 562 | // GET /verify?token= — the link in a verification email. |
| 563 | 563 | export function verifyPage(ctx: Ctx): Response { |
| 564 | const done = verifyEmail(ctx.app, ctx.url.searchParams.get("token") ?? ""); | |
| 565 | return page( | |
| 564 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 565 | const done = timePhase(ctx.request, "verify", () => verifyEmail(ctx.app, ctx.url.searchParams.get("token") ?? "")); | |
| 566 | return timePhase(ctx.request, "render", () => page( | |
| 566 | 567 | { |
| 567 | 568 | ctx, |
| 568 | 569 | title: done.ok ? "Email verified" : "Link not valid", |
| @@ -575,7 +576,7 @@ export function verifyPage(ctx: Ctx): Response { | ||
| 575 | 576 | <p><a href="/settings">Settings</a></p>`, |
| 576 | 577 | }, |
| 577 | 578 | done.ok ? 200 : 400, |
| 578 | ); | |
| 579 | )); | |
| 579 | 580 | } |
| 580 | 581 | |
| 581 | 582 | export const EDGE_NAME_MESSAGE = "Sign up under the name your platform sign-in gave you."; |
test/server-timing-sweep.test.ts −3
| @@ -30,13 +30,10 @@ const UNNAMED: Array<[path: string, issue: string]> = [ | ||
| 30 | 30 | ["/alice/forge/settings/labels", "#3390"], |
| 31 | 31 | ["/alice/forge/settings/prompts", "#3390"], |
| 32 | 32 | ["/alice/forge/tells", "#3322"], |
| 33 | ["/claim", "#3390"], | |
| 34 | 33 | ["/live/apps", "#3390"], |
| 35 | 34 | ["/live/on-you", "#3390"], |
| 36 | ["/recover", "#3390"], | |
| 37 | 35 | ["/signin/parent", "#3390"], |
| 38 | 36 | ["/signin/parent/callback", "#3390"], |
| 39 | ["/verify", "#3390"], | |
| 40 | 37 | ]; |
| 41 | 38 | |
| 42 | 39 | /** Long-lived streams and live fragments: no page, no phases to name. */ |
test/verify-timing.test.ts added +64
| @@ -0,0 +1,64 @@ | ||
| 1 | /** `/verify`, `/recover` and `/claim` name their phases in Server-Timing and answer under 50 ms, signed in (#4317). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createUser } from "../src/core/users.ts"; | |
| 8 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 9 | import { quietLogs } from "./helpers/globals.ts"; | |
| 10 | ||
| 11 | quietLogs(); | |
| 12 | ||
| 13 | let root = ""; | |
| 14 | let app: App; | |
| 15 | let servers: Servers; | |
| 16 | let cookie = ""; | |
| 17 | ||
| 18 | beforeAll(async () => { | |
| 19 | root = mkdtempSync(join(tmpdir(), "forge-verify-timing-")); | |
| 20 | app = createApp({ dataDir: root, port: 0 }); | |
| 21 | const u = await createUser(app.db, { | |
| 22 | origin: "seed", | |
| 23 | trusted: true, | |
| 24 | username: "alice", | |
| 25 | email: "[email protected]", | |
| 26 | password: "a-good-passphrase", | |
| 27 | adminIfFirst: true, | |
| 28 | }); | |
| 29 | if (!u.ok) throw new Error(u.error); | |
| 30 | servers = await serve(app); | |
| 31 | const signin = await fetch(`${servers.url}/signin`, { | |
| 32 | method: "POST", | |
| 33 | headers: { origin: servers.url }, | |
| 34 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 35 | redirect: "manual", | |
| 36 | }); | |
| 37 | cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 38 | }); | |
| 39 | afterAll(() => { | |
| 40 | servers.stop(); | |
| 41 | app.close(); | |
| 42 | rmSync(root, { recursive: true, force: true }); | |
| 43 | }); | |
| 44 | ||
| 45 | const PAGES: [string, string[]][] = [ | |
| 46 | ["/verify?token=nope", ["session", "verify", "render"]], | |
| 47 | ["/recover", ["session", "render"]], | |
| 48 | ["/recover?token=nope", ["session", "link", "render"]], | |
| 49 | ["/claim", ["session", "render"]], | |
| 50 | ]; | |
| 51 | ||
| 52 | for (const [path, names] of PAGES) | |
| 53 | test(`GET ${path} names its phases and answers under 50 ms`, async () => { | |
| 54 | const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); | |
| 55 | await (await get()).text(); // warm | |
| 56 | const res = await get(); | |
| 57 | await res.text(); | |
| 58 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 59 | for (const name of ["app", ...names]) | |
| 60 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 61 | const dur = (n: string) => Number(parts.find((p) => p.startsWith(`${n};`))!.split("=")[1]); | |
| 62 | expect(dur("app")).toBeLessThan(50); | |
| 63 | expect(names.reduce((s, n) => s + dur(n), 0)).toBeLessThanOrEqual(dur("app") + 1); | |
| 64 | }); |
new above
offline