diff --git a/docs/auth.md b/docs/auth.md index 5b5a16b..0f39dbf 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -65,8 +65,8 @@ receive the code, so `https:` and non-loopback `http:` are both refused. ### Minting a token directly -Open in a browser and press the button. The token is blurred -until you point at it, and one click selects the whole value to copy. +Open in a browser and press the button. The token is covered by +asterisks until you point at it, and one click selects the whole value to copy. The endpoint is built for that page and always answers in HTML — `POST` mints and returns `201` with the token in the page body; there is no JSON representation. A script therefore diff --git a/src/http/token-issue.ts b/src/http/token-issue.ts index 5d4d509..ac3a9a6 100644 --- a/src/http/token-issue.ts +++ b/src/http/token-issue.ts @@ -45,17 +45,21 @@ button:hover{filter:brightness(1.08)} :focus-visible{outline:2px solid var(--warn);outline-offset:3px} .token{margin:26px 0;border:1px solid var(--rule-2);background:var(--sheet);padding:18px 20px; font-family:var(--mono);font-size:14px;color:var(--ink);word-break:break-all;user-select:all} -/* The secret stays unreadable until asked for, so a screen share or a - screenshot of the rest of the page does not carry it away. The blur is - presentation only — the text underneath is still selectable, which is what - lets one click select the whole value with no script on the page. */ -.token.secret{filter:blur(5px);transition:filter .12s ease-out;cursor:pointer} -.token.secret:hover,.token.secret:focus,.token.secret:active{filter:none} +/* The secret stays covered until asked for, so a screen share or a screenshot + of the rest of the page does not carry it away. Only one of the two spans is + ever in the layout: the stand-in is dropped on reveal so it can never land in + a copy, and the value is dropped while covered so it cannot be read. The + swap hangs off :hover and :focus because this page bans script and has no + other way to respond to a reader. */ +.token.secret{cursor:pointer} +.token.secret .real{display:none} +.token.secret:hover .mask,.token.secret:focus .mask,.token.secret:active .mask{display:none} +.token.secret:hover .real,.token.secret:focus .real,.token.secret:active .real{display:inline} +.mask{color:var(--ink-2)} .token::selection{background:var(--warn);color:var(--stock)} .hint{font-size:13px;margin:-18px 0 26px} .hint kbd{font-family:var(--mono);font-size:11px;border:1px solid var(--rule-2);border-radius:2px; padding:1px 5px;color:var(--ink)} -@media (prefers-reduced-motion:reduce){.token.secret{transition:none}} .once{border-left:2px solid var(--warn);background:color-mix(in srgb,var(--warn) 5%,var(--sheet)); padding:16px 20px;margin-bottom:26px} .once b{display:block;font-family:var(--mono);font-size:10px;text-transform:uppercase; @@ -82,17 +86,23 @@ comparing Base pools. No wallet, no account, no email.

`, ); +/** Matches the value's width so revealing it does not reflow the page. */ +function cover(value: string): string { + return "*".repeat(value.length); +} + function issuedPage(token: string): string { + const stars = cover(token); return page( "Your DeepTrace token", `

Your token

Shown onceCopy it now. It is stored only as a hash, so it cannot be shown again. Losing it costs nothing — come back and take another.
-
${token}
-

Blurred until you point at it. One click selects the whole token, then +

${token}
+

Hidden until you point at it. One click selects the whole token, then Cmd/Ctrl+C copies it.

Export it, then follow the setup guide for your client:

-
export DEEPTRACE_TOKEN="${token}"
+
export DEEPTRACE_TOKEN="${token}"

Never paste it into a chat, put it in a URL, or commit it.

`, ); } diff --git a/tests/unit/token-issue.test.ts b/tests/unit/token-issue.test.ts index cd05afa..fadfe79 100644 --- a/tests/unit/token-issue.test.ts +++ b/tests/unit/token-issue.test.ts @@ -130,20 +130,26 @@ describe("self-serve token issuance", () => { } }); - it("keeps the issued token blurred until the reader asks for it", async () => { + it("covers the issued token with asterisks until the reader asks for it", async () => { const { runtime, origin } = await startServer(); try { const body = await (await fetch(`${origin}/auth`, { method: "POST" })).text(); + const token = extractToken(body); - // Blur is presentation only. The value must stay selectable underneath, - // or the click-then-copy path this page documents stops working, and it - // must stay script-free so the page keeps its own CSP. - expect(body).toContain('class="token secret"'); - expect(body).toContain("filter:blur("); + // Only one of the two spans is ever laid out: the asterisks leave on + // reveal so they can never land in a copy, and the value stays out of the + // layout while covered so it cannot be read off a screen share. Both + // swaps have to stay script-free, or the page loses its own CSP. + expect(token).not.toBe(""); + expect(body).toContain( + ``, + ); + expect(body).toContain(`${token}`); + expect(body).toContain(".token.secret .real{display:none}"); + expect(body).toMatch(/\.token\.secret:hover \.mask[^{]*\{display:none\}/u); + expect(body).toMatch(/\.token\.secret:hover \.real[^{]*\{display:inline\}/u); expect(body).toContain("user-select:all"); - expect(body).toMatch(/\.token\.secret:hover[^{]*\{filter:none\}/u); expect(body).not.toMatch(/